1
0
James Ketrenos c2f8e74265 Post game cleanup
Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
2022-03-17 16:31:24 -07:00

4334 lines
121 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 layout = require('./layout.js');
const MAX_SETTLEMENTS = 5;
const MAX_CITIES = 4;
const MAX_ROADS = 15;
const debug = {
audio: false,
get: true,
set: true,
update: false
};
let gameDB;
require("../db/games").then(function(db) {
gameDB = db;
});
function shuffleArray(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 audio = {};
const processTies = (players) => {
/* 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] = [];
}
slots[player.order].push(player);
});
let ties = false, position = 1;
const irstify = (position) => {
switch (position) {
case 1: return `1st`;
case 2: return `2nd`;
case 3: return `3rd`;
case 4: return `4th`;
default: return position;
}
}
console.log(`Slots: `, slots);
/* Reverse from high to low */
slots.reverse().forEach((slot) => {
if (slot.length !== 1) {
ties = true;
slot.forEach(player => {
player.orderRoll = 0; /* Ties have to be re-rolled */
player.position = irstify(position);
player.orderStatus = `Tied for ${irstify(position)}`;
player.tied = true;
});
} else {
slot[0].tied = false;
slot[0].position = irstify(position);
slot[0].orderStatus = `Placed in ${irstify(position)}.`;
}
position += slot.length
});
return ties;
}
const processGameOrder = (game, player, dice) => {
if (player.orderRoll) {
return `You have already rolled for game order and are not in a tie.`;
}
player.orderRoll = dice;
player.order = player.order * 6 + dice;
const players = [];
let doneRolling = true;
for (let key in game.players) {
if (!game.players[key].orderRoll) {
doneRolling = false;
}
players.push(game.players[key]);
}
/* If 'doneRolling' is FALSE then there are still players to roll */
if (!doneRolling) {
sendUpdateToPlayers(game, {
players: getFilteredPlayers(game),
chat: game.chat
})
return;
}
/* sort updated player.order into the array */
players.sort((A, B) => {
return B.order - A.order;
});
console.log(`Pre process ties: `, players);
if (processTies(players)) {
console.log(`${info}: There are ties in player rolls:`, players);
sendUpdateToPlayers(game, {
players: getFilteredPlayers(game),
chat: game.chat
});
return;
}
addChatMessage(game, null, `Player order set to ` +
players.map((player) => `${player.position}: ${player.name}`)
.join(', ') + `.`);
game.playerOrder = players.map(player => player.color);
game.state = 'initial-placement';
game.direction = 'forward';
game.turn = {
name: players[0].name,
color: players[0].color
};
setForSettlementPlacement(game, getValidCorners(game));
addActivity(game, null, `${game.robberName} Robber Robinson entered the scene as the nefarious robber!`);
addChatMessage(game, null, `Initial settlement placement has started!`);
addChatMessage(game, null, `It is ${game.turn.name}'s turn to place a settlement.`);4
sendUpdateToPlayers(game, {
players: getFilteredPlayers(game),
state: game.state,
direction: game.direction,
turn: game.turn,
chat: game.chat,
activities: game.activities
});
}
const roll = (game, session) => {
const player = session.player,
name = session.name ? session.name : "Unnamed";
switch (game.state) {
case "lobby": /* currently not available as roll is only after color is
* set for players */
addChatMessage(game, session, `${name} rolled ${Math.ceil(Math.random() * 6)}.`);
sendUpdateToPlayers(game, { chat: game.chat });
return;
case "game-order":
game.startTime = Date.now();
const dice = Math.ceil(Math.random() * 6);
addChatMessage(game, session, `${name} rolled ${dice}.`);
return processGameOrder(game, player, dice);
case "normal":
if (game.turn.color !== session.color) {
return `It is not your turn.`;
}
if (game.turn.roll) {
return `You already rolled this turn.`;
}
processRoll(game, session, [ Math.ceil(Math.random() * 6), Math.ceil(Math.random() * 6) ]);
sendUpdateToPlayers(game, { chat: game.chat });
return;
default:
return `Invalid game state (${game.state}) in roll.`;
}
}
const sessionFromColor = (game, color) => {
for (let key in game.sessions) {
if (game.sessions[key].color === color) {
return game.sessions[key];
}
}
}
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 (staticData.pips[index].roll === roll) {
if (game.robber === i) {
tiles.push({ robber: true, index: i });
} else {
tiles.push({ robber: false, index: i });
}
}
}
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 },
"robber": { wood: 0, brick: 0, sheep: 0, wheat: 0, stone: 0 }
};
/* Find which corners are on each tile */
tiles.forEach(tile => {
let shuffle = game.tileOrder[tile.index];
const resource = game.tiles[shuffle];
layout.tiles[tile.index].corners.forEach(cornerIndex => {
const active = game.placements.corners[cornerIndex];
if (active && active.color) {
const count = active.type === 'settlement' ? 1 : 2;
if (!tile.robber) {
receives[active.color][resource.type] += count;
} else {
trackTheft(game, active.color, robber, resource.type, count);
receives.robber[resource.type] += count;
}
}
})
});
const robber = [];
for (let color in receives) {
const entry = receives[color];
if (!entry.wood && !entry.brick && !entry.sheep && !entry.wheat && !entry.stone) {
continue;
}
let message = [], session;
for (let type in entry) {
if (entry[type] === 0) {
continue;
}
if (color !== 'robber') {
session = sessionFromColor(game, color);
session.player[type] += entry[type];
session.player.resources += entry[type];
message.push(`${entry[type]} ${type}`);
} else {
robber.push(`${entry[type]} ${type}`);
}
}
if (session) {
addChatMessage(game, session, `${session.name} receives ${message.join(', ')}.`);
}
}
if (robber.length) {
addChatMessage(game, null, `That pesky ${game.robberName} Robber Roberson stole ${robber.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, session, dice) => {
addChatMessage(game, session, `${session.name} rolled ${dice[0]}, ${dice[1]}.`);
game.turn.roll = dice[0] + dice[1];
if (game.turn.roll !== 7) {
distributeResources(game, game.turn.roll);
for (let id in game.sessions) {
if (game.sessions[id].player) {
sendUpdateToPlayer(game, game.sessions[id], {
private: game.sessions[id].player
});
}
}
sendUpdateToPlayers(game, {
turn: game.turn,
players: getFilteredPlayers(game),
chat: game.chat
});
return;
}
/* ROBBER Robber Robinson! */
game.turn.robberInAction = true;
delete game.turn.placedRobber;
const mustDiscard = [];
for (let id in game.sessions) {
const player = game.sessions[id].player;
if (player) {
let discard = player.stone + player.wheat + player.brick + player.wood + player.sheep;
if (discard > 7) {
discard = Math.floor(discard / 2);
player.mustDiscard = discard;
mustDiscard.push(player);
} else {
delete player.mustDiscard;
}
}
}
if (mustDiscard.length === 0) {
addChatMessage(game, null, `ROBBER! ${game.robberName} Robber Roberson has fled, and no one had to discard!`);
addChatMessage(game, null, `A new robber has arrived and must be placed by ${game.turn.name}.`);
game.turn.actions = [ 'place-robber' ];
game.turn.limits = { pips: [] };
for (let i = 0; i < 19; i++) {
if (i === game.robber) {
continue;
}
game.turn.limits.pips.push(i);
}
} else {
mustDiscard.forEach(player => {
addChatMessage(game, null, `The robber was rolled and ${player.name} must discard ${player.mustDiscard} resource cards!`);
for (let key in game.sessions) {
if (game.sessions[key].player === player) {
sendUpdateToPlayer(game, game.sessions[key], {
private: player
});
break;
}
}
});
}
sendUpdateToPlayers(game, {
turn: game.turn,
players: getFilteredPlayers(game),
chat: game.chat
});
}
const newPlayer = (color) => {
return {
roads: MAX_ROADS,
cities: MAX_CITIES,
settlements: MAX_SETTLEMENTS,
points: 0,
status: "Not active",
lastActive: 0,
resources: 0,
order: 0,
stone: 0,
wheat: 0,
sheep: 0,
wood: 0,
brick: 0,
army: 0,
development: [],
color: color,
name: "",
totalTime: 0,
turnStart: 0
};
}
const getSession = (game, id) => {
if (!game.sessions) {
game.sessions = {};
}
/* 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] = {
id: `[${id.substring(0, 8)}]`,
name: '',
color: '',
player: undefined,
lastActive: Date.now(),
live: true
};
}
const session = game.sessions[id];
session.lastActive = Date.now();
session.live = true;
if (session.player) {
session.player.live = true;
session.player.lastActive = session.lastActive;
}
/* Expire old unused sessions */
for (let _id in game.sessions) {
const _session = game.sessions[_id];
if (_session.color || _session.name || _session.player) {
continue;
}
if (_id === id) {
continue;
}
/* 60 minutes */
const age = Date.now() - _session.lastActive;
if (age > 60 * 60 * 1000) {
console.log(`${_session.id}: Expiring old session ${_id}: ${age/(60 * 1000)} minutes`);
delete game.sessions[_id];
if (_id in game.sessions) {
console.log('delete DID NOT WORK!');
}
}
}
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(`${info}: Creating backup of games/${id}`);
await writeFile(`games/${id}.bk`, JSON.stringify(game));
} catch (error) {
console.log(`Load or parse error from games/${id}:`, 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(`Saving 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);
}
/* Clear out cached names from player colors and rebuild them
* from the information in the saved game sessions */
for (let color in game.players) {
delete game.players[color].name;
game.players[color].status = 'Not active';
}
/* Reconnect session player colors to the player objects */
game.unselected = [];
for (let id in game.sessions) {
const session = game.sessions[id];
if (session.name && session.color && session.color in game.players) {
session.player = game.players[session.color];
session.player.name = session.name;
session.player.status = 'Active';
session.player.live = false;
} else {
session.color = '';
session.player = undefined;
}
session.live = false;
/* Populate the 'unselected' list from the session table */
if (!game.sessions[id].color && game.sessions[id].name) {
game.unselected.push(game.sessions[id]);
}
}
games[id] = game;
return game;
};
const clearPlayer = (player) => {
const color = player.color;
for (let key in player) {
delete player[key];
}
Object.assign(player, newPlayer(color));
}
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(', ')}.`
}
}
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;
setForRoadPlacement(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;
setForCityPlacement(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;
setForSettlementPlacement(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;
session.resources += 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':
addActivity(game, session, `${game.turn.name} rolled ${dice[0]}.`);
processGameOrder(game, session.player, dice[0]);
break;
case 'normal':
processRoll(game, session, dice);
break;
}
break;
case "pass":
let name = game.turn.name;
const next = getNextPlayerSession(game, name);
game.turn = {
name: next.player,
color: next.color
};
game.turns++;
startTurnTimer(game, next);
addChatMessage(game, null, `The admin skipped ${name}'s turn.`);
addChatMessage(game, null, `It is ${next.name}'s turn.`);
break;
case "kick":
switch (value) {
case 'orange': color = 'O'; break;
case 'red': color = 'R'; break;
case 'blue': color = 'B'; break;
case 'white': color = 'W'; break;
}
if (!color) {
return `Unable to find player ${value}`
}
player = game.players[color];
for (let id in game.sessions) {
const session = game.sessions[id];
if (session.player !== player) {
continue;
}
console.log(`Kicking ${value} from ${game.id}.`);
const preamble = session.name ? `${session.name}, playing as ${colorToWord(color)},` : colorToWord(color);
addChatMessage(game, null, `${preamble} was kicked from game by the Admin.`);
if (player) {
clearPlayer(player);
session.player = undefined;
}
session.color = "";
return;
}
return `Unable to find active session for ${colorToWord(color)} (${value})`;
default:
return `Invalid admin action ${action}.`;
}
};
const setPlayerName = (game, session, name) => {
if (session.name === name) {
return; /* no-op */
}
if (session.color) {
return `You cannot change your name while you have a color selected.`;
}
if (!name) {
return `You can not set your name to nothing!`;
}
if (name.toLowerCase() === 'the bank') {
return `You cannot play as the bank!`;
}
/* Check to ensure name is not already in use */
let rejoin = false;
for (let id in game.sessions) {
const tmp = game.sessions[id];
if (tmp === session || !tmp.name) {
continue;
}
if (tmp.name.toLowerCase() === name.toLowerCase()) {
if (!tmp.player || (Date.now() - tmp.player.lastActive) > 60000) {
rejoin = true;
/* Update the session object from tmp, but retain websocket
* from active session */
Object.assign(session, tmp, { ws: session.ws, id: session.id });
console.log(`${info}: ${name} has been reallocated to a new session.`);
delete game.sessions[id];
} else {
return `${name} is already taken and has been active in the last minute.`;
}
}
}
let message, hasAudio = false;
if (!session.name) {
message = `A new player has entered the lobby as ${name}.`;
} else {
if (rejoin) {
if (session.color) {
message = `${name} has reconnected to the game.`;
} else {
message = `${name} has rejoined the lobby.`;
}
session.name = name;
if (session.ws && (game.id in audio)
&& session.name in audio[game.id]) {
hasAudio = true;
part(audio[game.id], session);
}
} else {
message = `${session.name} has changed their name to ${name}.`;
if (session.ws && game.id in audio) {
part(audio[game.id], session);
}
}
}
session.name = name;
session.live = true;
if (session.player) {
session.color = session.player.color;
session.player.name = session.name;
session.player.status = `Active`;
session.player.lastActive = Date.now();
session.player.name = name;
session.player.live = true;
}
if (session.ws && hasAudio) {
join(audio[game.id], session);
}
console.log(`${info}: ${message}`);
addChatMessage(game, null, message);
/* Rebuild the unselected list */
if (!session.color) {
console.log(`${info}: Adding ${session.name} to the unselected`);
}
game.unselected = [];
for (let id in game.sessions) {
if (!game.sessions[id].color && game.sessions[id].name) {
game.unselected.push(game.sessions[id]);
}
}
sendUpdateToPlayer(game, session, {
name: session.name,
color: session.color,
live: session.live,
private: session.player
});
sendUpdateToPlayers(game, {
players: getFilteredPlayers(game),
unselected: getFilteredUnselected(game),
chat: game.chat
});
/* Now that a name is set, send the full game to the player */
sendGameToPlayer(game, session);
}
const colorToWord = (color) => {
switch (color) {
case 'O': return 'orange';
case 'W': return 'white';
case 'B': return 'blue';
case 'R': return 'red';
default:
return '';
}
}
const getActiveCount = (game) => {
let active = 0;
for (let color in game.players) {
if (!game.players[color].name) {
continue;
}
active++;
}
return active;
}
const setPlayerColor = (game, session, color) => {
/* Selecting the same color is a NO-OP */
if (session.color === color) {
return;
}
/* Verify the player has a name set */
if (!session.name) {
return `You may only select a player when you have set your name.`;
}
if (game.state !== 'lobby') {
return `You may only select a player when the game is in the lobby.`;
}
/* Verify selection is valid */
if (color && !(color in game.players)) {
return `An invalid player selection was attempted.`;
}
/* Verify selection is not already taken */
if (color && game.players[color].status !== 'Not active') {
return `${game.sessions[color].name} already has ${colorToWord(color)}`;
}
let active = getActiveCount(game);
if (session.player) {
/* Deselect currently active player for this session */
clearPlayer(session.player);
session.player = undefined;
session.color = '';
active--;
/* If the player is not selecting a color, then return */
if (!color) {
addChatMessage(game, null,
`${session.name} is no longer ${colorToWord(session.color)}.`);
game.unselected.push(session);
game.active = active;
if (active === 1) {
addChatMessage(game, null,
`There are no longer enough players to start a game.`);
}
sendUpdateToPlayer(game, session, {
name: session.name,
color: '',
live: session.live,
private: session.player
});
sendUpdateToPlayers(game, {
active: game.active,
unselected: getFilteredUnselected(game),
players: getFilteredPlayers(game),
chat: game.chat
});
return;
}
}
/* All good -- set this player to requested selection */
active++;
session.color = color;
session.live = true;
session.player = game.players[color];
session.player.name = session.name;
session.player.status = `Active`;
session.player.lastActive = Date.now();
session.player.live = true;
addChatMessage(game, session, `${session.name} has chosen to play as ${colorToWord(color)}.`);
const update = {
players: getFilteredPlayers(game),
chat: game.chat
};
/* Rebuild the unselected list */
const unselected = [];
for (let id in game.sessions) {
if (!game.sessions[id].color && game.sessions[id].name) {
unselected.push(game.sessions[id]);
}
}
if (unselected.length !== game.unselected.length) {
game.unselected = unselected;
update.unselected = getFilteredUnselected(game);
}
if (game.active !== active) {
if (game.active < 2 && active >= 2) {
addChatMessage(game, null,
`There are now enough players to start the game.`);
}
game.active = active;
update.active = game.active;
}
sendUpdateToPlayer(game, session, {
name: session.name,
color: session.color,
live: session.live,
private: session.player,
});
sendUpdateToPlayers(game, update);
};
const addActivity = (game, session, message) => {
let date = Date.now();
if (game.activities.length && game.activities[game.activities.length - 1].date === date) {
date++;
}
game.activities.push({ color: session ? session.color : '', message, date });
}
const addChatMessage = (game, session, message) => {
let now = Date.now();
let lastTime = 0;
if (game.chat.length) {
lastTime = game.chat[game.chat.length - 1].date;
}
if (now <= lastTime) {
now = lastTime + 1;
}
const entry = {
date: now,
message: message
};
if (session && session.name) {
entry.from = session.name;
}
if (session && session.color) {
entry.color = session.color;
}
game.chat.push(entry);
};
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 getNextPlayerSession = (game, name) => {
let color;
for (let id in game.sessions) {
if (game.sessions[id].name === name) {
color = game.sessions[id].color;
break;
}
}
let index = game.playerOrder.indexOf(color);
index = (index + 1) % game.playerOrder.length;
color = game.playerOrder[index];
for (let id in game.sessions) {
if (game.sessions[id].color === color) {
return game.sessions[id];
}
}
console.error(`getNextPlayerSession -- no player found!`);
console.log(game.players);
}
const getPrevPlayerSession = (game, name) => {
let color;
for (let id in game.sessions) {
if (game.sessions[id].name === name) {
color = game.sessions[id].color;
break;
}
}
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];
}
}
console.error(`getNextPlayerSession -- no player found!`);
console.log(game.players);
}
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 });
}
}
});
if (debug.road) 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;
}
});
});
if (debug.road) console.log('Graphs B:', graphs);
if (debug.road) 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);
if (debug.road) console.log('Post update:', game.placements.roads.filter(road => road.color));
let checkForTies = false;
if (debug.road) console.log(currentLongest, currentLength);
if (currentLongest && game.players[currentLongest].longestRoad < currentLength) {
const _session = sessionFromColor(game, currentLongest);
addChatMessage(game, session, `${session.name} had their longest road split!`);
checkForTies = true;
}
let longestRoad = 4, longestPlayers = [];
for (let key in game.players) {
const player = game.players[key];
if (player.status === 'Not active') {
continue;
}
if (player.longestRoad > longestRoad) {
longestPlayers = [ player ];
longestRoad = player.longestRoad;
} else if (game.players[key].longestRoad === longestRoad) {
if (longestRoad >= 5) {
longestPlayers.push(player);
}
}
}
console.log({ longestPlayers });
if (longestPlayers.length > 0) {
if (longestPlayers.length === 1) {
game.longestRoadLength = longestRoad;
if (game.longestRoad !== longestPlayers[0].color) {
game.longestRoad = longestPlayers[0].color;
addChatMessage(game, session,
`${longestPlayers[0].name} now has the longest ` +
`road (${longestRoad})!`);
}
} else {
if (checkForTies) {
game.longestRoadLength = longestRoad;
const names = longestPlayers.map(player => player.name);
addChatMessage(game, session, `${names.join(', ')} are tied for longest ` +
`road (${longestRoad})!`);
}
/* Do not reset the longest road! Current Longest is still longest! */
}
} else {
game.longestRoad = false;
game.longestRoadLength = 0;
}
};
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: 'Submitting 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 meet the offer */
const checkPlayerOffer = (game, player, offer) => {
let error = undefined;
const name = player.name;
console.log({ checkPlayerOffer: {
name: name,
player: player,
gets: offer.gets,
gives: offer.gives,
sheep: player.sheep,
wheat: player.wheat,
brick: player.brick,
stone: player.stone,
wood: player.wood,
description: offerToString(offer)
} });
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 = `${name} does do not have ${give.count} ${give.type}!`;
return;
}
if (offer.gets.find(get => give.type === get.type)) {
error = `${name} 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 = `${name} can not give and get the same resource type!`;
};
})
return error;
};
const canMeetOffer = (player, offer) => {
for (let i = 0; i < offer.gets.length; i++) {
const get = offer.gets[i];
if (get.type === 'bank') {
if (player[player.gives[0].type] < get.count) {
return false;
}
} else if (player[get.type] < get.count) {
return false;
}
}
return true;
};
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 setForRoadPlacement = (game, limits) => {
game.turn.actions = [ 'place-road' ];
game.turn.limits = { roads: limits };
}
const setForCityPlacement = (game, limits) => {
game.turn.actions = [ 'place-city' ];
game.turn.limits = { corners: limits };
}
const setForSettlementPlacement = (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 = 'Invalid request';
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);
}
if (!error) {
sendGameToPlayers(game);
}
}
return res.status(400).send(error);
});
const trade = (game, session, action, offer) => {
const name = session.name, player = session.player;
let warning;
if (game.state !== "normal") {
return `Game not in correct state to begin trading.`;
}
if (!game.turn.actions || game.turn.actions.indexOf('trade') === -1) {
/* Only the active player can begin trading */
if (game.turn.name !== name) {
return `You cannot start trading negotiations when it is not your turn.`
}
game.turn.actions = [ 'trade' ];
game.turn.limits = {};
for (let key in game.players) {
game.players[key].gives = [];
game.players[key].gets = [];
delete game.players[key].offerRejected;
}
addActivity(game, session, `${name} requested to begin trading negotiations.`);
return;
}
/* Only the active player can cancel trading */
if (action === 'cancel') {
/* TODO: Perhaps 'cancel' is how a player can remove an offer... */
if (game.turn.name !== name) {
return `Only the active player can cancel trading negotiations.`;
}
game.turn.actions = [];
game.turn.limits = {};
addActivity(game, session, `${name} has cancelled trading negotiations.`);
return;
}
/* Any player can make an offer */
if (action === 'offer') {
warning = checkPlayerOffer(game, session.player, offer);
if (warning) {
return warning;
}
if (isSameOffer(session.player, offer)) {
console.log(session.player);
return `You already have a pending offer submitted for ${offerToString(offer)}.`;
}
session.player.gives = offer.gives;
session.player.gets = offer.gets;
session.player.offerRejected = {};
if (game.turn.color === session.color) {
game.turn.offer = offer;
}
/* If this offer matches what another player wants, clear rejection
* on of that other player's offer */
for (let color in game.players) {
if (color === session.color) {
continue;
}
const other = game.players[color];
if (other.status !== 'Active') {
continue;
}
/* Comparison reverses give/get order */
if (isSameOffer(other, { gives: offer.gets, gets: offer.gives }) && other.offerRejected) {
console.log('clear rejection', other, offer);
delete other.offerRejected[session.color];
} else {
console.log('do not clear rejection', other, offer);
}
}
addActivity(game, session, `${session.name} submitted an offer to give ${offerToString(offer)}.`);
return;
}
/* Any player can reject an offer */
if (action === 'reject') {
/* If the active player rejected an offer, they rejected another player */
const other = game.players[offer.color];
if (!other.offerRejected) {
other.offerRejected = {};
}
other.offerRejected[session.color] = true;
addActivity(game, session, `${session.name} rejected ${other.name}'s offer.`);
return;
}
/* Only the active player can accept an offer */
if (action === 'accept') {
if (game.turn.name !== name) {
return `Only the active player can accept an offer.`;
}
let target;
console.log({ offer, description: offerToString(offer) });
warning = checkPlayerOffer(game, session.player, offer);
if (warning) {
return warning;
}
/* 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') {
target = game.players[offer.color];
if (offer.color in target.offerRejected) {
return `${target.name} rejected this offer.`;
}
if (!isCompatibleOffer(target, offer)) {
return `Unfortunately, trades were re-negotiated in transit and the deal is invalid!`;
}
warning = checkPlayerOffer(game, target, { gives: offer.gets, gets: offer.gives });
if (warning) {
return warning;
}
if (!isSameOffer(target, { gives: offer.gets, gets: offer.gives })) {
console.log( { target, offer });
return `These terms were not agreed to by ${target.name}!`;
}
if (!canMeetOffer(target, player)) {
return `${target.name} cannot meet the terms.`;
}
} else {
target = offer;
}
debugChat(game, 'Before trade');
/* Transfer goods */
offer.gets.forEach(item => {
if (target.name !== 'The bank') {
target[item.type] -= item.count;
target.resources -= item.count;
}
player[item.type] += item.count;
player.resources += item.count;
});
offer.gives.forEach(item => {
if (target.name !== 'The bank') {
target[item.type] += item.count;
target.resources += item.count;
}
player[item.type] -= item.count;
player.resources -= item.count;
});
const from = (offer.name === 'The bank') ? 'the bank' : offer.name;
addChatMessage(game, session, `${session.name} traded ` +
` ${offerToString(offer)} ` +
`from ${from}.`);
addActivity(game, session, `${session.name} accepted a trade from ${from}.`)
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 = [];
}
}
const clearTimeNotice= (game, session) => {
if (!session.player.turnNotice) {
/* benign state; don't alert the user */
//return `You have not been idle.`;
}
session.player.turnNotice = "";
sendUpdateToPlayer(game, session, {
private: session.player
});
};
const startTurnTimer = (game, session) => {
const timeout = 90;
if (!session.ws) {
console.log(`${session.id}: Aborting turn timer as ${session.name} is disconnected.`);
} else {
console.log(`${session.id}: (Re)setting turn timer for ${session.name} to ${timeout} seconds.`);
}
if (game.turnTimer) {
clearTimeout(game.turnTimer);
}
if (!session.connected) {
game.turnTimer = 0;
return;
}
game.turnTimer = setTimeout(() => {
console.log(`${session.id}: Turn timer expired for ${session.name}`);
session.player.turnNotice = 'It is still your turn.';
sendUpdateToPlayer(game, session, {
private: session.player
});
resetTurnTimer(game, session);
}, timeout * 1000);
}
const resetTurnTimer = (game, session) => {
startTurnTimer(game, session);
}
const stopTurnTimer = (game) => {
if (game.turnTimer) {
console.log(`${info}: Stopping turn timer.`);
clearTimeout(game.turnTimer);
game.turnTimer = 0;
}
}
const shuffle = (game, session) => {
if (game.state !== "lobby") {
return `Game no longer in lobby (${game.state}). Can not shuffle board.`;
}
if (game.turns > 0) {
return `Game already in progress (${game.turns} so far!) and cannot be shuffled.`;
}
shuffleBoard(game);
console.log(`${session.id}: Shuffled to new signature: ${game.signature}`);
sendUpdateToPlayers(game, {
pipOrder: game.pipOrder,
tileOrder: game.tileOrder,
borderOrder: game.borderOrder,
robber: game.robber,
robberName: game.robberName,
signature: game.signature
});
}
const pass = (game, session) => {
const name = session.name;
if (game.turn.name !== name) {
return `You cannot pass when it isn't your turn.`
}
/* 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) {
return `Robber is in action. Turn can not stop until all Robber tasks are resolved.`;
}
const next = getNextPlayerSession(game, session.name);
session.player.totalTime += Date.now() - session.player.turnStart;
session.player.turnNotice = "";
game.turn = {
name: next.name,
color: next.color
};
next.player.turnStart = Date.now();
startTurnTimer(game, next);
game.turns++;
addActivity(game, session, `${name} passed their turn.`);
addChatMessage(game, null, `It is ${next.name}'s turn.`);
sendUpdateToPlayer(game, next, {
private: next.player
});
sendUpdateToPlayer(game, session, {
private: session.player
});
sendUpdateToPlayers(game, {
turns: game.turns,
turn: game.turn,
chat: game.chat,
activities: game.activities
});
saveGame(game);
}
const placeRobber = (game, session, robber) => {
const name = session.name;
robber = parseInt(robber);
if (game.state !== 'normal' && game.turn.roll !== 7) {
return `You cannot place robber unless 7 was rolled!`;
}
if (game.turn.name !== name) {
return `You cannot place the robber when it isn't your turn.`;
}
for (let color in game.players) {
if (game.players[color].status === 'Not active') {
continue;
}
if (game.players[color].mustDiscard > 0) {
return `You cannot place the robber until everyone has discarded!`;
}
}
if (game.robber === robber) {
return `You must move the robber to a new location!`;
}
game.robber = robber;
game.turn.placedRobber = true;
pickRobber(game);
addActivity(game, null, `${game.robberName} Robber Robinson entered the scene as the nefarious robber!`);
let targets = [];
layout.tiles[robber].corners.forEach(cornerIndex => {
const active = game.placements.corners[cornerIndex];
if (active && active.color
&& active.color !== game.turn.color
&& targets.findIndex(item => item.color === active.color) === -1) {
targets.push({
color: active.color,
name: game.players[active.color].name
});
}
});
if (targets.length) {
game.turn.actions = [ 'steal-resource' ],
game.turn.limits = { players: targets };
} 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.`);
}
sendUpdateToPlayers(game, {
placements: game.placements,
turn: game.turn,
chat: game.chat,
robber: game.robber,
robberName: game.robberName,
activities: game.activities
});
sendUpdateToPlayer(game, session, {
private: session.player
});
}
const stealResource = (game, session, color) => {
if (game.turn.actions.indexOf('steal-resource') === -1) {
return `You can only steal a resource when it is valid to do so!`;
}
if (game.turn.limits.players.findIndex(item => item.color === color) === -1) {
return `You can only steal a resource from a player on this terrain!`;
}
let victim;
for (let key in game.sessions) {
if (game.sessions[key].color === color) {
victim = game.sessions[key];
break;
}
}
if (!victim) {
return `You sent a wierd color for the target to steal from.`;
}
const cards = [];
[ 'wheat', 'brick', 'sheep', 'stone', 'wood' ].forEach(field => {
for (let i = 0; i < victim.player[field]; i++) {
cards.push(field);
}
});
debugChat(game, 'Before steal');
if (cards.length === 0) {
addChatMessage(game, session,
`${victim.name} ` +
`did not have any cards for ${session.name} to steal.`);
game.turn.actions = [];
game.turn.limits = {};
} else {
let index = Math.floor(Math.random() * cards.length),
type = cards[index];
victim.player[type]--;
victim.player.resources--;
session.player[type]++;
session.player.resources++;
game.turn.actions = [];
game.turn.limits = {};
trackTheft(game, victim.color, session.color, type, 1);
addChatMessage(game, session,
`${session.name} randomly stole 1 ${type} from ` +
`${victim.name}.`);
sendUpdateToPlayer(game, victim, {
private: victim.player
});
}
debugChat(game, 'After steal');
game.turn.robberInAction = false;
sendUpdateToPlayer(game, session, {
private: session.player
});
sendUpdateToPlayers(game, {
turn: game.turn,
chat: game.chat,
activities: game.activities,
players: getFilteredPlayers(game)
});
}
const buyDevelopment = (game, session) => {
const player = session.player;
if (game.state !== 'normal') {
return `You cannot purchase a development card unless the game is active.`;
}
if (session.color !== game.turn.color) {
return `It is not your turn! It is ${game.turn.name}'s turn.`;
}
if (!game.turn.roll) {
return `You cannot build until you have rolled.`;
}
if (game.turn && game.turn.robberInAction) {
return `Robber is in action. You can not purchase until all Robber tasks are resolved.`;
}
if (game.developmentCards.length < 1) {
return `There are no more development cards!`;
}
if (player.stone < 1 || player.wheat < 1 || player.sheep < 1) {
return `You have insufficient resources to purchase a development card.`;
}
if (game.turn.developmentPurchased) {
return `You have already purchased a development card this turn.`;
}
debugChat(game, 'Before development purchase');
addActivity(game, session, `${session.name} purchased a development card.`);
addChatMessage(game, session, `${session.name} spent 1 stone, 1 wheat, 1 sheep to purchase a development card.`)
player.stone--;
player.wheat--;
player.sheep--;
player.resources = 0;
[ 'wheat', 'brick', 'sheep', 'stone', 'wood' ].forEach(resource => {
player.resources += player[resource];
});
debugChat(game, 'After development purchase');
const card = game.developmentCards.pop();
card.turn = game.turns;
player.development.push(card);
sendUpdateToPlayer(game, session, {
private: session.player
});
sendUpdateToPlayers(game, {
chat: game.chat,
activities: game.activities,
players: getFilteredPlayers(game)
});
}
const playCard = (game, session, card) => {
const name = session.name, player = session.player;
if (game.state !== 'normal') {
return `You cannot play a development card unless the game is active.`;
}
if (session.color !== game.turn.color) {
return `It is not your turn! It is ${game.turn.name}'s turn.`;
}
if (!game.turn.roll) {
return `You cannot play a card until you have rolled.`;
}
if (game.turn && game.turn.robberInAction) {
return `Robber is in action. You can not play a card until all Robber tasks are resolved.`;
}
card = player.development.find(item => item.type == card.type && item.card == card.card);
if (!card) {
return `The card you want to play was not found in your hand!`;
}
if (player.playedCard === game.turns && card.type !== 'vp') {
return `You can only play one development card per turn!`;
}
if (card.played) {
return `You have already played this card.`;
}
/* 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) {
return `You can not play victory point cards until you can reach 10!`;
}
addChatMessage(game, session, `${name} played a Victory Point card.`);
}
if (card.type === 'progress') {
switch (card.card) {
case 'road-1':
case 'road-2':
const allowed = Math.min(player.roads, 2);
if (!allowed) {
addChatMessage(game, session, `${session.name} played a Road Building card, but has no roads to build.`);
break;
}
let roads = getValidRoads(game, session.color);
if (roads.length === 0) {
addChatMessage(game, session, `${session.name} played a Road Building card, but they do not have any valid locations to place them.`);
break;
}
game.turn.active = 'road-building';
game.turn.free = true;
game.turn.freeRoads = allowed;
addChatMessage(game, session, `${session.name} played a Road Building card. They now place ${allowed} roads for free.`);
setForRoadPlacement(game, roads);
break;
case 'monopoly':
game.turn.actions = [ 'select-resources' ];
game.turn.active = 'monopoly';
addActivity(game, session, `${session.name} played the Monopoly card, and is selecting their resource type to claim.`);
break;
case 'year-of-plenty':
game.turn.actions = [ 'select-resources' ];
game.turn.active = 'year-of-plenty';
addActivity(game, session, `${session.name} played the Year of Plenty card.`);
break;
default:
addActivity(game, session, `Oh no! ${card.card} isn't impmented yet!`);
break;
}
}
card.played = true;
player.playedCard = game.turns;
if (card.type === 'army') {
player.army++;
addChatMessage(game, session, `${session.name} played a Knight and must move the robber!`);
if (player.army > 2 &&
(!game.largestArmy || game.players[game.largestArmy].army < player.army)) {
if (game.largestArmy !== session.color) {
game.largestArmy = session.color;
game.largestArmySize = player.army;
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);
}
}
sendUpdateToPlayer(game, session, {
private: session.player
});
sendUpdateToPlayers(game, {
chat: game.chat,
activities: game.activities,
largestArmy: game.largestArmy,
largestArmySize: game.largestArmySize,
turn: game.turn,
players: getFilteredPlayers(game)
});
}
const placeSettlement = (game, session, index) => {
const player = session.player;
index = parseInt(index);
if (game.state !== 'initial-placement' && game.state !== 'normal') {
return `You cannot place an item unless the game is active.`;
}
if (session.color !== game.turn.color) {
return `It is not your turn! It is ${game.turn.name}'s turn.`;
}
if (game.placements.corners[index] === undefined) {
return `You have requested to place a settlement illegally!`;
}
/* 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) {
return `You tried to cheat! You should not try to break the rules.`;
}
const corner = game.placements.corners[index];
if (corner.color) {
return `This location already has a settlement belonging to ${game.players[corner.color].name}!`;
}
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) {
return `You have insufficient resources to build a settlement.`;
}
}
if (player.settlements < 1) {
return `You have already built all of your settlements.`;
}
player.settlements--;
if (!game.turn.free) {
addChatMessage(game, session, `${session.name} spent 1 brick, 1 wood, 1 sheep, 1 wheat to purchase a settlement.`)
player.brick--;
player.wood--;
player.wheat--;
player.sheep--;
player.resources = 0;
[ 'wheat', 'brick', 'sheep', 'stone', 'wood' ].forEach(resource => {
player.resources += player[resource];
});
}
delete game.turn.free;
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(`${session.id}: Bank ${bank} = ${type}`);
if (!type) {
console.log(`${session.id}: Bank ${bank}`)
return;
}
bankType = (type === 'bank')
? '3 of anything for 1 resource'
: `2 ${type} for 1 resource`;
if (player.banks.indexOf(type) === -1) {
player.banks.push(type);
}
});
}
game.turn.actions = [];
game.turn.limits = {};
if (bankType) {
addActivity(game, session,
`${session.name} placed a settlement by a maritime bank that trades ${bankType}.`);
} else {
addActivity(game, session, `${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 => {
const border = game.borderOrder[Math.floor(bank / 3)],
type = game.borders[border][bank % 3];
console.log(`${session.id}: Bank ${bank} = ${type}`);
if (!type) {
return;
}
bankType = (type === 'bank')
? '3 of anything for 1 resource'
: `2 ${type} for 1 resource`;
if (player.banks.indexOf(type) === -1) {
player.banks.push(type);
}
});
}
player.settlements--;
if (bankType) {
addActivity(game, session,
`${session.name} placed a settlement by a maritime bank that trades ${bankType}. ` +
`Next, they need to place a road.`);
} else {
addActivity(game, session, `${session.name} placed a settlement. ` +
`Next, they need to place a road.`);
}
setForRoadPlacement(game, layout.corners[index].roads);
}
sendUpdateToPlayer(game, session, {
private: session.player
});
sendUpdateToPlayers(game, {
placements: game.placements,
activities: game.activities,
turn: game.turn,
chat: game.chat
});
}
const placeRoad = (game, session, index) => {
const player = session.player;
index = parseInt(index);
if (game.state !== 'initial-placement' && game.state !== 'normal') {
return `You cannot place an item unless the game is active.`;
}
if (session.color !== game.turn.color) {
return `It is not your turn! It is ${game.turn.name}'s turn.`;
}
if (game.placements.roads[index] === undefined) {
return `You have requested to place a road illegally!`;
}
/* 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) {
return `You tried to cheat! You should not try to break the rules.`;
}
const road = game.placements.roads[index];
if (road.color) {
return `This location already has a road belonging to ${game.players[road.color].name}!`;
}
if (game.state === 'normal') {
if (!game.turn.free) {
if (player.brick < 1 || player.wood < 1) {
return `You have insufficient resources to build a road.`;
}
}
if (player.roads < 1) {
return `You have already built all of your roads.`;
}
debugChat(game, 'Before road purchase');
player.roads--;
if (!game.turn.free) {
addChatMessage(game, session, `${session.name} spent 1 brick, 1 wood to purchase a road.`)
player.brick--;
player.wood--;
player.resources = 0;
[ 'wheat', 'brick', 'sheep', 'stone', 'wood' ].forEach(resource => {
player.resources += player[resource];
});
}
debugChat(game, 'After road purchase');
road.color = session.color;
addActivity(game, session, `${session.name} placed a road.`);
calculateRoadLengths(game, session);
let resetLimits = true;
if (game.turn.active === 'road-building') {
game.turn.freeRoads--;
if (game.turn.freeRoads === 0) {
delete game.turn.free;
delete game.turn.active;
delete game.turn.freeRaods;
}
let roads = getValidRoads(game, session.color);
if (roads.length === 0) {
delete game.turn.active;
delete game.turn.freeRaods;
addActivity(game, session, `${session.name} has another road to play, but there are no more valid locations.`);
} else if (game.turn.freeRoads !== 0) {
game.turn.free = true;
setForRoadPlacement(game, roads);
resetLimits = false;
}
}
if (resetLimits) {
delete game.turn.free;
game.turn.actions = [];
game.turn.limits = {};
}
} else if (game.state === 'initial-placement') {
road.color = session.color;
addActivity(game, session, `${session.name} placed a road.`);
calculateRoadLengths(game, session);
let next;
if (game.direction === 'forward' && getLastPlayerName(game) === session.name) {
game.direction = 'backward';
next = session.player;
} else if (game.direction === 'backward' && getFirstPlayerName(game) === session.name) {
/* Done! */
delete game.direction;
} else {
if (game.direction === 'forward') {
next = getNextPlayerSession(game, session.name);
} else {
next = getPrevPlayerSession(game, session.name);
}
}
if (next) {
game.turn = {
name: next.name,
color: next.color
};
startTurnTimer(game, next);
setForSettlementPlacement(game, getValidCorners(game));
calculateRoadLengths(game, session);
addChatMessage(game, null, `It is ${next.name}'s turn to place a settlement.`);
} else {
game.turn = {
actions: [],
limits: { },
name: session.name,
color: getColorFromName(game, session.name)
};
session.player.turnStart = Date.now();
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];
player.resources += receives[type];
sendUpdateToPlayer(game, session, {
private: player
});
message.push(`${receives[type]} ${type}`);
}
addChatMessage(game, session, `${session.name} receives ${message.join(', ')}.`);
}
}
addChatMessage(game, null, `It is ${session.name}'s turn.`);
game.state = 'normal';
}
}
sendUpdateToPlayer(game, session, {
private: session.player
});
sendUpdateToPlayers(game, {
placements: game.placements,
turn: game.turn,
chat: game.chat,
state: game.state,
longestRoad: game.longestRoad,
longestRoadLength: game.longestRoadLength,
players: getFilteredPlayers(game)
});
}
const discard = (game, session, discards) => {
const player = session.player;
if (game.turn.roll !== 7) {
return `You can only discard due to the Robber!`;
}
let sum = 0;
for (let type in discards) {
if (player[type] < parseInt(discards[type])) {
return `You have requested to discard more ${type} than you have.`
}
sum += parseInt(discards[type]);
}
if (sum !== player.mustDiscard) {
return `You need to discard ${player.mustDiscard} cards.`;
}
for (let type in discards) {
const count = parseInt(discards[type]);
player[type] -= count;
player.mustDiscard -= count;
player.resources -= count;
}
addChatMessage(game, null, `${session.name} discarded ${sum} resource cards.`);
if (player.mustDiscard) {
addChatMessage(game, null, `${session.name} did not discard enough and must discard ${player.mustDiscard} more cards.`);
}
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);
}
}
sendUpdateToPlayer(game, session, {
private: player
});
sendUpdateToPlayers(game, {
players: getFilteredPlayers(game),
chat: game.chat,
turn: game.turn
});
}
const buyRoad = (game, session) => {
const player = session.player;
if (game.state !== 'normal') {
return `You cannot purchase a road unless the game is active.`;
}
if (session.color !== game.turn.color) {
return `It is not your turn! It is ${game.turn.name}'s turn.`;
}
if (!game.turn.roll) {
return `You cannot build until you have rolled.`;
}
if (game.turn && game.turn.robberInAction) {
return `Robber is in action. You can not purchase until all Robber tasks are resolved.`;
}
if (player.brick < 1 || player.wood < 1) {
return `You have insufficient resources to build a road.`;
}
if (player.roads < 1) {
return `You have already built all of your roads.`;
}
const roads = getValidRoads(game, session.color);
if (roads.length === 0) {
return `There are no valid locations for you to place a road.`;
}
setForRoadPlacement(game, roads);
addActivity(game, session, `${game.turn.name} is considering building a road.`);
sendUpdateToPlayers(game, {
turn: game.turn,
chat: game.chat
});
}
const selectResources = (game, session, cards) => {
const player = session.player;
if (!game || !game.turn || !game.turn.actions ||
game.turn.actions.indexOf('select-resources') === -1) {
return `Please, let's not cheat. Ok?`;
}
if (session.color !== game.turn.color) {
return `It is not your turn! It is ${game.turn.name}'s turn.`;
}
const count = (game.turn.active === 'monopoly') ? 1 : 2;
if (!cards || cards.length > count || cards.length === 0) {
return `You have chosen the wrong number of cards!`;
}
const isValidCard = (type) => {
switch (type.trim()) {
case 'wheat':
case 'brick':
case 'sheep':
case 'stone':
case 'wood':
return true;
default:
return false;
};
}
const selected = {};
cards.forEach(card => {
if (!isValidCard(card)) {
return `Invalid resource type!`;
}
if (card in selected) {
selected[card]++;
} else {
selected[card] = 1;
}
});
const display = [];
for (let card in selected) {
display.push(`${selected[card]} ${card}`);
}
addActivity(game, session, `${session.name} has chosen ${display.join(', ')}!`);
switch (game.turn.active) {
case 'monopoly':
const gave = [], type = cards[0];
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(`${player.name} gave ${player[type]} ${type}`);
session.player[type] += player[type];
session.resources += player[type];
total += player[type];
player[type] = 0;
for (let key in game.sessions) {
if (game.sessions[key].player === player) {
sendUpdateToPlayer(game, game.sessions[key], {
private: game.sessions[key].player
});
break;
}
}
}
}
if (gave.length) {
addChatMessage(game, session, `${session.name} player Monopoly and selected ${display.join(', ')}. ` +
`Players ${gave.join(', ')}. In total, they received ${total} ${type}.`);
} else {
addActivity(game, session, 'No players had that resource. Wa-waaaa.');
}
break;
case 'year-of-plenty':
cards.forEach(type => {
session.player[type]++;
session.player.resources++;
});
addChatMessage(game, session, `${session.name} player Year of Plenty.` +
`They chose to receive ${display.join(', ')} from the bank.`);
break;
}
delete game.turn.active;
game.turn.actions = [];
sendUpdateToPlayer(game, session, {
private: session.player
});
sendUpdateToPlayers(game, {
turn: game.turn,
chat: game.chat,
activities: game.activities
});
}
const buySettlement = (game, session) => {
const player = session.player;
if (game.state !== 'normal') {
return `You cannot purchase a settlement unless the game is active.`;
}
if (session.color !== game.turn.color) {
return `It is not your turn! It is ${game.turn.name}'s turn.`;
}
if (!game.turn.roll) {
return `You cannot build until you have rolled.`;
}
if (game.turn && game.turn.robberInAction) {
return `Robber is in action. You can not purchase until all Robber tasks are resolved.`;
}
if (player.brick < 1 || player.wood < 1 || player.wheat < 1 || player.sheep < 1) {
return `You have insufficient resources to build a settlement.`;
}
if (player.settlements < 1) {
return `You have already built all of your settlements.`;
}
const corners = getValidCorners(game, session.color);
if (corners.length === 0) {
return `There are no valid locations for you to place a settlement.`;
}
setForSettlementPlacement(game, corners);
addActivity(game, session, `${game.turn.name} is considering placing a settlement.`);
sendUpdateToPlayers(game, {
turn: game.turn,
chat: game.chat,
activities: game.activities
});
}
const buyCity = (game, session) => {
const player = session.player;
if (game.state !== 'normal') {
return `You cannot purchase a city unless the game is active.`;
}
if (session.color !== game.turn.color) {
return `It is not your turn! It is ${game.turn.name}'s turn.`;
}
if (!game.turn.roll) {
return `You cannot build until you have rolled.`;
}
if (player.wheat < 2 || player.stone < 3) {
return `You have insufficient resources to build a city.`;
}
if (game.turn && game.turn.robberInAction) {
return `Robber is in action. You can not purchase until all Robber tasks are resolved.`;
}
if (player.city < 1) {
return `You have already built all of your cities.`;
}
const corners = getValidCorners(game, session.color, 'settlement');
if (corners.length === 0) {
return `There are no valid locations for you to place a city.`;
}
setForCityPlacement(game, corners);
addActivity(game, session, `${game.turn.name} is considering upgrading a settlement to a city.`);
sendUpdateToPlayers(game, {
turn: game.turn,
chat: game.chat,
activities: game.activities
});
}
const placeCity = (game, session, index) => {
const player = session.player;
index = parseInt(index);
if (game.state !== 'normal') {
return `You cannot place an item unless the game is active.`;
}
if (session.color !== game.turn.color) {
return `It is not your turn! It is ${game.turn.name}'s turn.`;
}
if (game.placements.corners[index] === undefined) {
return `You have requested to place a city illegally!`;
}
/* 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) {
return `You tried to cheat! You should not try to break the rules.`;
}
const corner = game.placements.corners[index];
if (corner.color !== session.color) {
return `This location already has a settlement belonging to ${game.players[corner.color].name}!`;
}
if (corner.type !== 'settlement') {
return `This location already has a city!`;
}
if (!game.turn.free) {
if (player.wheat < 2 || player.stone < 3) {
return `You have insufficient resources to build a city.`;
}
}
if (player.city < 1) {
return `You have already built all of your cities.`;
}
corner.color = session.color;
corner.type = 'city';
debugChat(game, 'Before city purchase');
player.cities--;
player.settlements++;
if (!game.turn.free) {
addChatMessage(game, session, `${session.name} spent 2 wheat, 3 stone to upgrade to a city.`)
player.wheat -= 2;
player.stone -= 3;
player.resources = 0;
[ 'wheat', 'brick', 'sheep', 'stone', 'wood' ].forEach(resource => {
player.resources += player[resource];
});
}
delete game.turn.free;
debugChat(game, 'After city purchase');
game.turn.actions = [];
game.turn.limits = {};
addActivity(game, session, `${session.name} upgraded a settlement to a city!`);
sendUpdateToPlayer(game, session, {
private: session.player
});
sendUpdateToPlayers(game, {
placements: game.placements,
turn: game.turn,
chat: game.chat,
activities: game.activities
});
}
const ping = (session) => {
if (!session.ws) {
console.log(`[no socket] Not sending ping to ${session.name} -- connection does not exist.`);
return;
}
session.ping = Date.now();
// console.log(`Sending ping to ${session.name}`);
session.ws.send(JSON.stringify({ type: 'ping', ping: session.ping }));
if (session.keepAlive) {
clearTimeout(session.keepAlive);
}
session.keepAlive = setTimeout(() => { ping(session); }, 2500);
}
const wsInactive = (game, req) => {
const session = getSession(game, req.cookies.player);
if (session && session.ws) {
console.log(`Closing WebSocket to ${session.name} due to inactivity.`);
session.ws.close();
session.ws = undefined;
}
/* Prevent future pings */
if (req.keepAlive) {
clearTimeout(req.keepAlive);
}
}
const setGameState = (game, session, state) => {
if (!state) {
return `Invalid state.`;
}
if (!session.color) {
return `You must have an active player to start the game.`;
}
if (state === game.state) {
return;
}
switch (state) {
case "game-order":
if (game.state !== 'lobby') {
return `You can only start the game from the lobby.`;
}
const active = getActiveCount(game);
if (active < 2) {
return `You need at least two players to start the game.`;
}
/* Delete any non-played colors from the player map; reduces all
* code that would otherwise have to filter out players by checking
* the 'Not active' state of player.status */
for (let key in game.players) {
if (game.players[key].status !== 'Active') {
delete game.players[key];
}
}
addChatMessage(game, null, `${session.name} requested to start the game.`);
game.state = state;
sendUpdateToPlayers(game, {
state: game.state,
chat: game.chat
});
break;
}
}
const resetDisconnectCheck = (game, req) => {
if (req.disconnectCheck) {
clearTimeout(req.disconnectCheck);
}
//req.disconnectCheck = setTimeout(() => { wsInactive(game, req) }, 20000);
}
const join = (peers, session) => {
const ws = session.ws;
if (!session.name) {
console.error(`${session.id}: <- join - No name set yet. Audio not available.`);
return;
}
console.log(`${session.id}: <- join - ${session.name}`);
console.log(`${all}: -> addPeer - ${session.name}`);
if (session.name in peers) {
console.log(`${session.id}:${session.name} - Already joined to Audio.`);
return;
}
for (let peer in peers) {
peers[peer].send(JSON.stringify({
type: 'addPeer',
data: { 'peer_id': session.name, 'should_create_offer': false }
}));
ws.send(JSON.stringify({
type: 'addPeer',
data: {'peer_id': peer, 'should_create_offer': true}
}));
}
/* Add this user as a peer connected to this WebSocket */
peers[session.name] = ws;
};
const part = (peers, session) => {
const ws = session.ws;
if (!session.name) {
console.error(`${session.id}: <- part - No name set yet. Audio not available.`);
return;
}
if (!(session.name in peers)) {
console.log(`${session.id}: <- ${session.name} - Does not exist in game audio.`);
return;
}
console.log(`${session.id}: <- ${session.name} - Audio part.`);
console.log(`${all}: -> removePeer - ${session.name}`);
delete peers[session.name];
/* Remove this peer from all other peers, and remove each
* peer from this peer */
for (let peer in peers) {
peers[peer].send(JSON.stringify({
type: 'removePeer',
data: {'peer_id': session.name}
}));
ws.send(JSON.stringify({
type: 'removePeer',
data: {'peer_id': session.name}
}));
}
};
const getName = (session) => {
return session.name ? session.name : session.id;
}
const saveGame = async (game) => {
/* 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);
}
delete reducedGame.turnTimer;
delete reducedGame.unselected;
/* Save per turn while debugging... */
game.step = game.step ? game.step : 0;
await writeFile(`games/${game.id}.${game.step++}`, JSON.stringify(reducedGame, null, 2))
.catch((error) => {
console.error(`${session.id} Unable to write to games/${game.id}`);
console.error(error);
});
await writeFile(`games/${game.id}`, JSON.stringify(reducedGame, null, 2))
.catch((error) => {
console.error(`${session.id} Unable to write to games/${game.id}`);
console.error(error);
});
}
const departLobby = (game, session, color) => {
const update = {};
update.unselected = getFilteredUnselected(game);
if (session.player) {
session.player.live = false;
update.players = game.players;
}
if (session.name) {
if (session.color) {
addChatMessage(game, null, `${session.name} has disconnected ` +
`from the game.`);
} else {
addChatMessage(game, null, `${session.name} has left the lobby.`);
}
update.chat = game.chat;
} else {
console.log(`${session.id}: departLobby - ${getName(session)} is ` +
`being removed from ${game.id}'s sessions.`);
for (let id in game.sessions) {
if (game.sessions[id] === session) {
delete game.sessions[id];
break;
}
}
}
sendUpdateToPlayers(game, update);
}
const all = `[ all ]`;
const info = `[ info ]`;
const todo = `[ todo ]`;
const sendGameToPlayer = (game, session) => {
console.log(`${session.id}: -> sendGamePlayer:${getName(session)} - full game`);
if (!session.ws) {
console.log(`${session.id}: -> sendGamePlayer:: Currently no connection`);
return;
}
let update;
/* Only send empty name data to unnamed players */
if (!session.name) {
console.log(`${session.id}: -> sendGamePlayer:${getName(session)} - only sending empty name`);
update = { name: "" };
} else {
update = getFilteredGameForPlayer(game, session);
}
session.ws.send(JSON.stringify({
type: 'game-update',
update: update
}));
};
const sendGameToPlayers = (game) => {
console.log(`${all}: -> sendGamePlayers - full game`);
for (let key in game.sessions) {
sendGameToPlayer(game, game.sessions[key]);
}
};
const sendUpdateToPlayers = async (game, update) => {
/* Ensure clearing of a field actually gets sent by setting
* undefined to 'false'
*/
for (let key in update) {
if (update[key] === undefined) {
update[key] = false;
}
}
calculatePoints(game, update);
if (debug.update) {
console.log(`[ all ]: -> sendUpdateToPlayers - `, update);
} else {
const keys = Object.getOwnPropertyNames(update);
console.log(`[ all ]: -> sendUpdateToPlayers - ${keys.join(',')}`);
}
const message = JSON.stringify({
type: 'game-update',
update
});
for (let key in game.sessions) {
const _session = game.sessions[key];
/* Only send player and game data to named players */
if (!_session.name) {
console.log(`${session.id}: -> sendUpdateToPlayers:` +
`${getName(_session)} - only sending empty name`);
message = JSON.stringify({ name: "" });
}
if (!_session.ws) {
console.log(`${_session.id}: -> sendUpdateToPlayers: ` +
`Currently no connection.`);
} else {
_session.ws.send(message);
}
}
}
const sendUpdateToPlayer = async (game, session, update) => {
/* If this player does not have a name, *ONLY* send the name, regardless
* of what is requested */
if (!session.name) {
console.log(`${session.id}: -> sendUpdateToPlayer:${getName(session)} - only sending empty name`);
update = { name: "" };
}
/* Ensure clearing of a field actually gets sent by setting
* undefined to 'false'
*/
for (let key in update) {
if (update[key] === undefined) {
update[key] = false;
}
}
calculatePoints(game, update);
if (debug.update) {
console.log(`${session.id}: -> sendUpdateToPlayer:${getName(session)} - `, update);
} else {
const keys = Object.getOwnPropertyNames(update);
console.log(`${session.id}: -> sendUpdateToPlayer:${getName(session)} - ${keys.join(',')}`);
}
const message = JSON.stringify({
type: 'game-update',
update
});
if (!session.ws) {
console.log(`${session.id}: -> sendUpdateToPlayer: Currently no connection.`);
} else {
session.ws.send(message);
}
}
const getFilteredUnselected = (game) => {
if (!game.unselected) {
return [];
}
return game.unselected
.filter(session => session.live)
.map(session => session.name);
}
const parseChatCommands = (game, message) => {
/* Chat messages can set game flags and fields */
const parts = message.match(/^set +([^ ]*) +(.*)$/i);
if (!parts || parts.length !== 3) {
return;
}
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;
}
};
const sendError = (session, error) => {
session.ws.send(JSON.stringify({ type: 'error', error }));
}
const sendWarning = (session, warning) => {
session.ws.send(JSON.stringify({ type: 'warning', warning }));
}
const getFilteredPlayers = (game) => {
const filtered = {};
for (let color in game.players) {
const player = Object.assign({}, game.players[color]);
filtered[color] = player;
if (player.status === 'Not active') {
continue;
}
player.resources = 0;
[ 'wheat', 'brick', 'sheep', 'stone', 'wood' ].forEach(resource => {
player.resources += player[resource];
delete player[resource];
});
delete player.development;
}
return filtered;
};
const calculatePoints = (game, update) => {
if (game.state === 'winner') {
return;
}
/* 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;
}
const currentPoints = player.points;
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 (player.points === currentPoints) {
return;
}
if (player.points < 10) {
update.players = getFilteredPlayers(game);
continue;
}
/* This player has 10 points! Check if they are the current
* player and if so, declare victory! */
console.log(`${info}: Whoa! ${player.name} has ${player.points}!`);
for (let key in game.sessions) {
if (game.sessions[key].color !== player.color
|| game.sessions[key].status === 'Not active') {
continue;
}
const message = `Wahoo! ${player.name} has 10 points on their turn and has won!`;
addChatMessage(game, null, message)
console.log(`${info}: ${message}`);
update.winner = Object.assign({}, player, {
state: 'winner',
stolen: game.stolen,
chat: game.chat,
turns: game.turns,
players: game.players,
elapsedTime: Date.now() - game.startTime
});
game.winner = update.winner;
game.state = 'winner';
game.waiting = [];
stopTurnTimer(game);
sendUpdateToPlayers(game, {
state: game.state,
winner: game.winner,
players: game.players /* unfiltered */
});
}
}
/* 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;
}
}
}
const gotoLobby = (game, session) => {
if (!game.waiting) {
game.waiting = [];
}
const already = game.waiting.indexOf(session.name) !== -1;
const waitingFor = [];
for (let key in game.sessions) {
if (game.sessions[key] === session) {
continue;
}
if (game.sessions[key].player && game.waiting.indexOf(game.sessions[key].name) == -1) {
waitingFor.push(game.sessions[key].name);
}
}
if (!already) {
game.waiting.push(session.name);
addChatMessage(game, null, `${session.name} has gone to the lobby.`);
} else if (waitingFor.length !== 0) {
return `You are already waiting in the lobby. ` +
`${waitingFor.join(',')} still needs to go to the lobby.`;
}
if (waitingFor.length === 0) {
resetGame(game);
addChatMessage(game, null, `All players are back to the lobby.`);
addChatMessage(game, null,
`The game has been reset. You can play again with this board, or `+
`click 'New board' to mix things up a bit.`);
sendGameToPlayers(game);
return;
}
addChatMessage(game, null, `Waiting for ${waitingFor.join(',')} to go to lobby.`);
sendUpdateToPlayers(game, {
chat: game.chat
});
}
router.ws("/ws/:id", async (ws, req) => {
if (!req.cookies || !req.cookies.player) {
ws.send({ type: 'error', error: `Unable to find session cookie` });
return;
}
const { id } = req.params;
const gameId = id;
const short = `[${req.cookies.player.substring(0, 8)}]`;
ws.id = short;
console.log(`${short}: Game ${gameId} - New connection from client.`);
if (!(id in audio)) {
audio[id] = {}; /* List of peer sockets using session.name as index. */
console.log(`${short}: Game ${id} - New Game Audio`);
} else {
console.log(`${short}: Game ${id} - Already has Audio`);
}
/* Setup WebSocket event handlers prior to performing any async calls or
* we may miss the first messages from clients */
ws.on('error', async (event) => {
console.error(`WebSocket error: `, event.message);
const game = await loadGame(gameId);
if (!game) {
return;
}
const session = getSession(game, req.cookies.player);
session.live = false;
if (session.ws) {
session.ws.close();
session.ws = undefined;
}
departLobby(game, session);
});
ws.on('close', async (event) => {
console.log(`${short} - closed connection`);
const game = await loadGame(gameId);
if (!game) {
return;
}
const session = getSession(game, req.cookies.player);
if (session.player) {
session.player.live = false;
}
session.live = false;
if (session.ws) {
/* Cleanup any voice channels */
if (id in audio) {
part(audio[id], session);
}
session.ws.close();
session.ws = undefined;
console.log(`${short}:WebSocket closed for ${getName(session)}`);
}
departLobby(game, session);
/* Check for a game in the Winner state with no more connections
* and remove it */
if (game.state === 'winner' || game.state === 'lobby') {
let dead = true;
for (let id in game.sessions) {
if (game.sessions[id].live && game.sessions[id].color) {
dead = false;
}
}
if (dead) {
console.log(`${session.id}: No more players in ${game.id}. Removing.`);
addChatMessage(game, null, `No more active players in game. ` +
`It is being removed from the server.`);
sendUpdateToPlayers(game, {
chat: game.chat
});
for (let id in game.sessions) {
if (game.sessions[id].ws) {
game.sessions[id].ws.close();
delete game.sessions[id];
}
}
delete audio[id];
delete games[id];
try {
fs.unlinkSync(`games/${id}`);
} catch (error) {
console.error(`${session.id}: Unable to remove games/${id}`);
}
}
}
});
ws.on('message', async (message) => {
let data;
try {
data = JSON.parse(message);
} catch (error) {
console.error(`${session.id}: parse error`, message);
return;
}
const game = await loadGame(gameId);
const session = getSession(game, req.cookies.player);
if (!session.ws) {
session.ws = ws;
}
if (session.player) {
session.player.live = true;
}
session.live = true;
session.lastActive = Date.now();
let error, warning, update, processed = true;
switch (data.type) {
case 'join':
join(audio[id], session);
break;
case 'part':
part(audio[id], session);
break;
case 'relayICECandidate': {
if (!(id in audio)) {
console.error(`${session.id}:${id} <- relayICECandidate - Does not have Audio`);
return;
}
const { peer_id, candidate } = data.config;
if (debug.audio) console.log(`${short}:${id} <- relayICECandidate ${getName(session)} to ${peer_id}`,
candidate);
message = JSON.stringify({
type: 'iceCandidate',
data: {'peer_id': getName(session), 'candidate': candidate }
});
if (peer_id in audio[id]) {
audio[id][peer_id].send(message);
}
} break;
case 'relaySessionDescription': {
if (!(id in audio)) {
console.error(`${id} - relaySessionDescription - Does not have Audio`);
return;
}
const { peer_id, session_description } = data.config;
if (debug.audio) console.log(`${short}:${id} - relaySessionDescription ${getName(session)} to ${peer_id}`,
session_description);
message = JSON.stringify({
type: 'sessionDescription',
data: {'peer_id': getName(session), 'session_description': session_description }
});
if (peer_id in audio[id]) {
audio[id][peer_id].send(message);
}
} break;
case 'pong':
resetDisconnectCheck(game, req);
break;
case 'game-update':
console.log(`${short}: <- game-update ${getName(session)} - full game update.`);
sendGameToPlayer(game, session);
break;
case 'player-name':
console.log(`${short}: <- player-name:${getName(session)} - setPlayerName - ${data.name}`)
error = setPlayerName(game, session, data.name);
if (error) {
sendError(session, error);
}else {
saveGame(game);
}
break;
case 'set':
console.log(`${short}: <- set:${getName(session)} ${data.field} = ${data.value}`);
switch (data.field) {
case 'state':
warning = setGameState(game, session, data.value);
if (warning) {
sendWarning(session, warning);
} else {
saveGame(game);
}
break;
case 'color':
warning = setPlayerColor(game, session, data.value);
if (warning) {
sendWarning(session, warning);
} else {
saveGame(game);
}
break;
default:
console.warn(`WARNING: Requested SET unsupported field: ${data.field}`);
break;
}
break;
case 'get':
console.log(`${short}: <- get:${getName(session)} ${data.fields.join(',')}`);
update = {};
data.fields.forEach((field) => {
switch (field) {
case 'player':
sendWarning(session, `'player' is not a valid item. use 'private' instead`);
update.player = undefined;
break;
case 'id':
case 'chat':
case 'startTime':
case 'state':
case 'turn':
case 'turns':
case 'winner':
update[field] = game[field];
break;
case 'name':
update.name = session.name;
break;
case 'unselected':
update.unselected = getFilteredUnselected(game);
break;
case 'private':
update.private = session.player;
break;
case 'players':
update.players = getFilteredPlayers(game);
break;
case 'color':
console.log(`${session.id}: -> Returning color as ${session.color} for ${getName(session)}`);
update.color = session.color;
break;
case 'timestamp':
update.timestamp = Date.now();
break;
default:
if (field in game) {
console.warn(`${short}: WARNING: Requested GET not-privatized/sanitized field: ${field}`);
update[field] = game[field];
} else {
if (field in session) {
console.warn(`${short}: WARNING: Requested GET not-sanitized session field: ${field}`);
update[field] = session[field];
} else {
console.warn(`${short}: WARNING: Requested GET unsupported field: ${field}`);
}
}
break;
}
});
sendUpdateToPlayer(game, session, update);
break;
case 'chat':
console.log(`${short}:${id} - ${data.type} - ${data.message}`)
addChatMessage(game, session, `${session.name}: ${data.message}`);
parseChatCommands(game, data.message);
sendUpdateToPlayers(game, { chat: game.chat });
saveGame(game);
break;
default:
processed = false;
break;
}
if (processed) {
/* saveGame(game); -- do not save here; only save on changes */
return;
}
/* The rest of the actions and commands require an active game
* participant */
if (!session.player) {
error = `Player must have an active color.`;
sendError(session, error);
return;
}
processed = true;
const priorSession = session;
switch (data.type) {
case 'roll':
console.log(`${short}: <- roll:${getName(session)}`);
warning = roll(game, session);
if (warning) {
sendWarning(session, warning);
}
break;
case 'shuffle':
console.log(`${short}: <- shuffle:${getName(session)}`);
warning = shuffle(game, session);
if (warning) {
warning(session, error);
}
break;
case 'place-settlement':
console.log(`${short}: <- place-settlement:${getName(session)} ${data.index}`);
warning = placeSettlement(game, session, data.index);
if (warning) {
sendWarning(session, warning);
}
break;
case 'place-city':
console.log(`${short}: <- place-city:${getName(session)} ${data.index}`);
warning = placeCity(game, session, data.index);
if (warning) {
sendWarning(session, warning);
}
break;
case 'place-road':
console.log(`${short}: <- place-road:${getName(session)} ${data.index}`);
warning = placeRoad(game, session, data.index);
if (warning) {
sendWarning(session, warning);
}
break;
case 'place-robber':
console.log(`${short}: <- place-robber:${getName(session)} ${data.index}`);
warning = placeRobber(game, session, data.index);
if (warning) {
sendWarning(session, warning);
}
break;
case 'steal-resource':
console.log(`${short}: <- steal-resource:${getName(session)} ${data.color}`);
warning = stealResource(game, session, data.color);
if (warning) {
sendWarning(session, warning);
}
break;
case 'discard':
console.log(`${short}: <- discard:${getName(session)}`);
warning = discard(game, session, data.discards);
if (warning) {
sendWarning(session, warning);
}
break;
case 'pass':
console.log(`${short}: <- pass:${getName(session)}`);
warning = pass(game, session);
if (warning) {
sendWarning(session, warning);
}
break;
case 'select-resources':
console.log(`${short}: <- select-resources:${getName(session)} - `, data.cards);
warning = selectResources(game, session, data.cards);
if (warning) {
sendWarning(session, warning);
}
break;
case 'buy-city':
console.log(`${short}: <- buy-city:${getName(session)}`);
warning = buyCity(game, session);
if (warning) {
sendWarning(session, warning);
}
break;
case 'buy-road':
console.log(`${short}: <- buy-road:${getName(session)}`);
warning = buyRoad(game, session);
if (warning) {
sendWarning(session, warning);
}
break;
case 'buy-settlement':
console.log(`${short}: <- buy-settlement:${getName(session)}`);
warning = buySettlement(game, session);
if (warning) {
sendWarning(session, warning);
}
break;
case 'buy-development':
console.log(`${short}: <- buy-development:${getName(session)}`);
warning = buyDevelopment(game, session);
if (warning) {
sendWarning(session, warning);
}
break;
case 'play-card':
console.log(`${short}: <- play-card:${getName(session)}`);
warning = playCard(game, session, data.card);
if (warning) {
sendWarning(session, warning);
}
break;
case 'trade':
console.log(`${short}: <- trade:${getName(session)} - ` +
(data.action ? data.action : 'start') + ` - `,
data.offer ? data.offer : 'no trade yet');
warning = trade(game, session, data.action, data.offer);
if (warning) {
sendWarning(session, warning);
} else {
for (let key in game.sessions) {
const tmp = game.sessions[key];
if (tmp.player) {
sendUpdateToPlayer(game, tmp, {
private: tmp.player
});
}
}
sendUpdateToPlayer(game, session, {
private: session.player
});
sendUpdateToPlayers(game, {
players: getFilteredPlayers(game),
turn: game.turn,
activities: game.activities,
chat: game.chat
});
}
break;
case 'turn-notice':
console.log(`${short}: <- turn-notice:${getName(session)}`);
warning = clearTimeNotice(game, session);
if (warning) {
sendWarning(session, warning);
}
break;
case 'goto-lobby':
console.log(`${short}: <- goto-lobby:${getName(session)}`);
warning = gotoLobby(game, session);
if (warning) {
sendWarning(session, warning);
}
break;
default:
console.warn(`Unsupported request: ${data.type}`);
processed = false;
break;
}
/* If action was taken, persist the game */
if (processed) {
saveGame(game);
}
/* If the current player took an action, reset the session timer */
if (processed && session.color === game.turn.color && game.state !== 'winner') {
resetTurnTimer(game, session);
}
});
/* This will result in the node tick moving forward; if we haven't already
* setup the event handlers, a 'message' could come through prior to this
* completing */
const game = await loadGame(gameId);
if (!game) {
console.error(`Unable to load/create new game for WS request.`);
return;
}
const session = getSession(game, req.cookies.player);
session.ws = ws;
if (session.player) {
session.player.live = true;
}
session.live = true;
session.lastActive = Date.now();
if (session.name) {
sendUpdateToPlayers(game, {
players: getFilteredPlayers(game),
unselected: getFilteredUnselected(game)
});
}
/* If the current turn player just rejoined, set their turn timer */
if (game.turn && game.turn.color === session.color && game.state !== 'winner') {
resetTurnTimer(game, session);
}
if (session.name) {
if (session.color) {
addChatMessage(game, null, `${session.name} has reconnected to the game.`);
} else {
addChatMessage(game, null, `${session.name} has rejoined the lobby.`);
}
sendUpdateToPlayers(game, { chat: game.chat });
}
resetDisconnectCheck(game, req);
console.log(`${short}: Game ${id} - WebSocket connect from ${getName(session)}`);
if (session.keepAlive) {
clearTimeout(session.keepAlive);
}
session.keepAlive = setTimeout(() => { ping(session); }, 2500);
});
const debugChat = (game, preamble) => {
preamble = `Degug ${preamble.trim()}`;
let playerInventory = preamble;
for (let key in game.players) {
const player = game.players[key];
if (player.status === 'Not active') {
continue;
}
if (playerInventory !== '') {
playerInventory += ' player';
} else {
playerInventory += ' Player'
}
playerInventory += ` ${player.name} has `;
const has = [ 'wheat', 'brick', 'sheep', 'stone', 'wood' ].map(resource => {
const count = player[resource] ? player[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 getFilteredGameForPlayer = (game, session) => {
/* 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);
}
const player = session.player ? session.player : undefined;
/* Strip out data that should not be shared with players */
delete reducedGame.developmentCards;
/* Delete the game timer */
delete reducedGame.turnTimer;
reducedGame.unselected = getFilteredUnselected(game);
return Object.assign(reducedGame, {
live: true,
timestamp: Date.now(),
status: session.error ? session.error : "success",
name: session.name,
color: session.color,
order: (session.color in game.players) ? game.players[session.color].order : 0,
private: player,
sessions: reducedSessions,
layout: layout
});
}
/* Example:
"stolen": {
"robber": {
"stole": {
"total": 5,
"wheat": 2,
"wood": 1,
"sheep": 2
}
},
"O": {
"stolen": {
"total": 2,
"wheat": 2
},
"stole": {
"total": 2,
"brick": 2
}
},
"W": {
"stolen": {
"total": 4,
"brick": 2,
"wood": 1,
"sheep": 2
},
"stole": {
"total": 3,
"brick": 2,
"wheat": 1
}
}
}
*/
const trackTheft = (game, from, to, type, count) => {
const stats = game.stolen;
/* Initialize the stole / stolen structures */
[ to, from ].forEach(player => {
if (!(player in stats)) {
stats[player] = {
stole: { /* the resources this player stole */
total: 0
},
stolen: { /* the resources stolen from this player */
total: 0
}
};
}
});
/* Initialize 'type' field in structures */
if (!(type in stats[from].stole)) {
games.stolen[from].stole[type] = 0;
}
if (!(type in stats[to].stole)) {
games.stole[to].stolen[type] = 0;
}
/* Update counts */
stats[from].stolen.total += count;
stats[from].stolen[type] += count;
stats[to].stole.total += count;
stats[to].stole[type] += count;
}
const resetGame = (game) => {
Object.assign(game, {
startTime: Date.now(),
state: 'lobby',
turns: 0,
step: 0, /* used for the suffix # in game backups */
turn: {},
sheep: 19,
ore: 19,
wool: 19,
brick: 19,
wheat: 19,
placements: {
corners: [],
roads: []
},
developmentCards: [],
chat: [],
activities: [],
pipOrder: game.pipOrder,
borderOrder: game.borderOrder,
tileOrder: game.tileOrder,
signature: game.signature,
players: game.players,
stolen: {
robber: {
stole: 0
},
total: 0
},
longestRoad: '',
longestRoadLength: 0,
largestArmy: '',
largestArmySize: 0,
winner: undefined,
active: 0
});
stopTurnTimer(game);
/* 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,
longestRoad: undefined
};
}
/* Put the robber back on the Desert */
for (let i = 0; i < game.pipOrder.length; i++) {
if (game.pipOrder[i] === 18) {
game.robber = i;
break;
}
}
/* Populate the game development cards with a fresh deck */
for (let i = 1; i <= 14; i++) {
game.developmentCards.push({
type: 'army',
card: i
});
}
[ 'monopoly', 'monopoly', 'road-1', 'road-2', 'year-of-plenty', '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
}));
shuffleArray(game.developmentCards);
/* Reset all player data, and add in any missing colors */
[ 'R', 'B', 'W', 'O' ].forEach(color => {
if (color in game.players) {
clearPlayer(game.players[color]);
} else {
game.players[color] = newPlayer(color);
}
});
/* Ensure sessions are connected to player objects */
for (let key in game.sessions) {
const session = game.sessions[key];
if (session.color) {
game.active++;
session.player = game.players[session.color];
session.player.status = 'Active';
session.player.lastActive = Date.now();
session.player.live = session.live;
session.player.name = session.name;
session.player.color = session.color;
}
}
}
const createGame = (id) => {
/* Look for a new game with random words that does not already exist */
while (!id) {
id = randomWords(4).join('-');
try {
/* If file can be read, it already exists so look for a new name */
accessSync(`games/${id}`, fs.F_OK);
id = '';
} catch (error) {
break;
}
}
console.log(`${info}: creating ${id}`);
const game = {
id: id,
developmentCards: [],
players: {
O: newPlayer('O'),
R: newPlayer('R'),
B: newPlayer('B'),
W: newPlayer('W')
},
sessions: {},
unselected: [],
active: 0,
step: 0 /* used for the suffix # in game backups */
};
[ "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;
audio[game.id] = {};
return game;
};
const setBeginnerGame = (game) => {
pickRobber(game);
shuffleArray(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);
}
shuffleArray(seq);
game.borderOrder = seq.slice();
for (let i = 6; i < 19; i++) {
seq.push(i);
}
shuffleArray(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++;
}
}
shuffleArray(game.developmentCards);
game.signature = gameSignature(game);
}
/* Simple NO-OP to set session cookie so player-id can use it as the
* index */
router.get("/", (req, res/*, next*/) => {
let playerId;
if (!req.cookies.player) {
playerId = crypto.randomBytes(16).toString('hex');
res.cookie('player', playerId);
} else {
playerId = req.cookies.player;
}
console.log(`[${playerId.substring(0, 8)}]: Browser hand-shake achieved.`);
return res.status(200).send({ player: playerId });
});
router.post("/:id?", async (req, res/*, next*/) => {
const { id } = req.params;
let playerId;
if (!req.cookies.player) {
playerId = crypto.randomBytes(16).toString('hex');
res.cookie('player', playerId);
} else {
playerId = req.cookies.player;
}
if (id) {
console.log(`[${playerId.substring(0,8)}]: Attempting load of ${id}`);
} else {
console.log(`[${playerId.substring(0,8)}]: Creating new game.`);
}
const game = await loadGame(id); /* will create game if it doesn't exist */
console.log(`[${playerId.substring(0,8)}]: ${game.id} loaded.`);
return res.status(200).send({ id: game.id });
});
module.exports = router;