1158 lines
29 KiB
JavaScript
Executable File
1158 lines
29 KiB
JavaScript
Executable File
"use strict";
|
|
|
|
const express = require("express"),
|
|
crypto = require("crypto"),
|
|
{ readFile, writeFile } = require("fs").promises,
|
|
fs = require("fs"),
|
|
accessSync = fs.accessSync,
|
|
randomWords = require("random-words");
|
|
|
|
let gameDB;
|
|
|
|
require("../db/games").then(function(db) {
|
|
gameDB = db;
|
|
});
|
|
|
|
const router = express.Router();
|
|
|
|
function shuffle(array) {
|
|
var currentIndex = array.length, temporaryValue, randomIndex;
|
|
|
|
// While there remain elements to shuffle...
|
|
while (0 !== currentIndex) {
|
|
|
|
// Pick a remaining element...
|
|
randomIndex = Math.floor(Math.random() * currentIndex);
|
|
currentIndex -= 1;
|
|
|
|
// And swap it with the current element.
|
|
temporaryValue = array[currentIndex];
|
|
array[currentIndex] = array[randomIndex];
|
|
array[randomIndex] = temporaryValue;
|
|
}
|
|
|
|
return array;
|
|
}
|
|
|
|
/* Board Tiles:
|
|
* 0 1 2
|
|
* 3 4 5 6
|
|
* 7 8 9 10 11
|
|
* 12 13 14 15
|
|
* 16 17 18
|
|
*/
|
|
|
|
/*
|
|
* c0
|
|
* /\
|
|
* r0 / \r1
|
|
* c6 / \ c1
|
|
* | |
|
|
* r6| p,a | r2
|
|
* c5| | c3
|
|
* \ /
|
|
* r5 \ / r3
|
|
* \/
|
|
* c4
|
|
*/
|
|
|
|
/* |
|
|
* 1 2 |
|
|
* 0 1| 3| 5|
|
|
* \. / \ / \ / \ 3
|
|
* \. 0/ 1\ 3/ 4\ 6/ 7\
|
|
* \./ \ / \ / \
|
|
* 0| 2| 4| |6
|
|
* 17 2| 0 5| 1 8| 2 |9 4
|
|
* 8| 10| 12| |14
|
|
* / \ / \ / \ / \
|
|
* 10/ 11\ 13/ 14\ 16/ 17\ 19/ 20\
|
|
* / \ / \ / \ / \
|
|
* 7| 9| 11| 13| |15
|
|
* 16 12| 3 15| 4 18| 5 21| 6 |22
|
|
* 17| 19| 21| 23| |25 5
|
|
* / \ / \ / \ / \ / \ ,/
|
|
* 23/ 24\ 26/ 27\ 29/ 30\ 32/ 33\ 35/ 36\ ,/
|
|
* / \ / \ / \ / \ / \ ,/
|
|
* 16| 18| 20| 22| 24| |26
|
|
* 15 25| 7 28| 8 31| 9 34| 10 37| 11 |38 6
|
|
* 27| 29| 31| 33| 35| |37
|
|
* /' \ / \ / \ / \ / \ /
|
|
* /' 39\ 40/ 41\ 43/ 44\ 46/ 47\ 49/ 50\ /53
|
|
* /' \ / \ / \ / \ / \ / 7
|
|
* 28| 30| 32| 34| |36
|
|
* 14 42| 12 45| 13 48| 14 51| 15 |52
|
|
* 38| 40| 42| 44| |46
|
|
* \ / \ / \ / \ /
|
|
* 54\ 55/ 56\ 58/ 59\ 61/ 62\ /65
|
|
* \ / \ / \ / \ / 8
|
|
* 39| 41| 43| |45
|
|
* 13 57| 16 60| 17 63| 18 |64
|
|
* 47| 49| 51| |53
|
|
* \ / \ / \ / `\
|
|
* 66\ 67/ 68\ 69/ 70\ /71 `\
|
|
* \ / \ / \ / `\
|
|
* 48| 50| 52| 9
|
|
* |
|
|
* 12 | 11 10
|
|
*/
|
|
const Tile = (corners, roads) => {
|
|
return {
|
|
corners: corners, /* 6 */
|
|
pip: -1,
|
|
roads: roads,
|
|
asset: -1
|
|
};
|
|
};
|
|
|
|
/* Borders have three sections each, so they are numbered
|
|
* 0-17 clockwise. Some corners share two borders. */
|
|
|
|
const Corner = (roads, banks) => {
|
|
return {
|
|
roads: roads, /* max of 3 */
|
|
banks: banks, /* max of 2 */
|
|
data: undefined
|
|
};
|
|
};
|
|
|
|
const Road = (corners) => {
|
|
return {
|
|
corners: corners, /* 2 */
|
|
data: undefined
|
|
}
|
|
};
|
|
|
|
const layout = {
|
|
tiles: [
|
|
Tile([ 0, 1, 2, 10, 9, 8], [ 0, 1, 5, 13, 11, 2]),
|
|
Tile([ 2, 3, 4, 12, 11, 10], [ 3, 4, 8, 16, 14, 5]),
|
|
Tile([ 4, 5, 6, 14, 13, 12], [ 6, 7, 9, 19, 17, 8]),
|
|
|
|
Tile([ 7, 8, 9, 19, 18, 17], [ 10, 11, 15, 26, 24, 12]),
|
|
Tile([ 9, 10, 11, 21, 20, 19], [ 13, 14, 18, 29, 27, 15]),
|
|
Tile([ 11, 12, 13, 23, 22, 21], [ 16, 17, 21, 32, 30, 18]),
|
|
Tile([ 13, 14, 15, 25, 24, 23], [ 19, 20, 22, 35, 33, 21]),
|
|
|
|
Tile([ 16, 17, 18, 29, 28, 27], [ 23, 24, 18, 40, 39, 25]),
|
|
Tile([ 18, 19, 20, 31, 30, 29], [ 26, 27, 31, 43, 41, 28]),
|
|
Tile([ 20, 21, 22, 33, 32, 31], [ 29, 30, 34, 46, 44, 31]),
|
|
Tile([ 22, 23, 24, 35, 34, 33], [ 32, 33, 37, 49, 47, 34]),
|
|
Tile([ 24, 25, 26, 37, 36, 35], [ 35, 36, 38, 53, 50, 37]),
|
|
|
|
Tile([ 28, 29, 30, 40, 39, 38], [ 40, 41, 45, 55, 54, 42]),
|
|
Tile([ 30, 31, 32, 42, 41, 40], [ 43, 44, 48, 58, 56, 45]),
|
|
Tile([ 32, 33, 34, 44, 43, 42], [ 46, 47, 51, 61, 59, 48]),
|
|
Tile([ 34, 35, 36, 46, 45, 44], [ 49, 50, 52, 65, 62, 51]),
|
|
|
|
Tile([ 39, 40, 41, 49, 48, 47], [ 55, 56, 60, 67, 66, 57]),
|
|
Tile([ 41, 42, 43, 51, 50, 49], [ 58, 59, 63, 69, 68, 60]),
|
|
Tile([ 43, 44, 45, 53, 52, 51], [ 61, 62, 64, 71, 70, 63])
|
|
],
|
|
roads: [
|
|
/* 0 */
|
|
Road([0, 1]),
|
|
Road([1, 2]),
|
|
Road([0, 8]),
|
|
Road([2, 3]),
|
|
Road([3, 4]),
|
|
Road([2, 10]),
|
|
Road([4, 5]),
|
|
Road([5, 6]),
|
|
Road([4, 12]),
|
|
Road([6, 14]),
|
|
/* 10 */
|
|
Road([8, 7]),
|
|
Road([8, 9]),
|
|
Road([7, 17]),
|
|
Road([9, 10]),
|
|
Road([10, 11]),
|
|
Road([9, 19]),
|
|
Road([12, 11]),
|
|
Road([12, 13]),
|
|
Road([11, 21]),
|
|
Road([14, 13]),
|
|
/* 20 */
|
|
Road([14, 15]),
|
|
Road([13,23 ]),
|
|
Road([15, 25]),
|
|
Road([17, 16]),
|
|
Road([17, 18]),
|
|
Road([16, 27]),
|
|
Road([19, 18]),
|
|
Road([19, 20]),
|
|
Road([18, 29]),
|
|
Road([21, 20]),
|
|
/* 30 */
|
|
Road([21, 22]),
|
|
Road([20, 31]),
|
|
Road([23, 22]),
|
|
Road([23, 24]),
|
|
Road([22,33]),
|
|
Road([25,24]),
|
|
Road([25,26]),
|
|
Road([24, 35]),
|
|
Road([26,37]),
|
|
Road([27,28]),
|
|
/* 40 */
|
|
Road([29,28]),
|
|
Road([29,30]),
|
|
Road([28,38]),
|
|
Road([31,30]),
|
|
Road([31,32]),
|
|
Road([30,40]),
|
|
Road([33,32]),
|
|
Road([33,34]),
|
|
Road([32,42]),
|
|
Road([35,34]),
|
|
/* 50 */
|
|
Road([35,36]),
|
|
Road([34,44]),
|
|
Road([36,46]),
|
|
Road([37,36]),
|
|
Road([38,39]),
|
|
Road([40,39]),
|
|
Road([40,41]),
|
|
Road([39,47]),
|
|
Road([41,42]),
|
|
Road([42,43]),
|
|
/* 60 */
|
|
Road([41,49]),
|
|
Road([44,43]),
|
|
Road([44,45]),
|
|
Road([43,51]),
|
|
Road([45,53]),
|
|
Road([46,45]),
|
|
Road([47,48]),
|
|
Road([49,48]),
|
|
Road([49,50]),
|
|
Road([51,50]),
|
|
/* 70 */
|
|
Road([51,52]),
|
|
Road([53,52]),
|
|
],
|
|
corners: [
|
|
/* 0 */
|
|
/* 0 */ Corner([2, 0],[17,0]),
|
|
/* 1 */ Corner([0, 1],[0,1]),
|
|
/* 2 */ Corner([1,3,5],[1]),
|
|
/* 3 */ Corner([3,4],[1,2]),
|
|
/* 4 */ Corner([8,4,6],[2]),
|
|
/* 5 */ Corner([6,7],[2,3]),
|
|
/* 6 */ Corner([7,9],[3,4]),
|
|
/* 7 */ Corner([12,10],[16,17]),
|
|
/* 8 */ Corner([2,10,11],[17]),
|
|
/* 9 */ Corner([11,13,15],[]),
|
|
/* 10 */
|
|
/* 10 */ Corner([5,13,14],[]),
|
|
/* 11 */ Corner([14,16,18],[]),
|
|
/* 12 */ Corner([8,16,17],[]),
|
|
/* 13 */ Corner([17,19,21],[]),
|
|
/* 14 */ Corner([9,19,20],[4]),
|
|
/* 15 */ Corner([20,22],[4,5]),
|
|
/* 16 */ Corner([23,25],[16,15]),
|
|
/* 17 */ Corner([12,23,24],[16]),
|
|
/* 18 */ Corner([24,26,28],[]),
|
|
/* 19 */ Corner([15,26,27],[]),
|
|
/* 20 */
|
|
/* 20 */ Corner([27,29,31],[]),
|
|
/* 21 */ Corner([18,29,30],[]),
|
|
/* 22 */ Corner([30,32,34],[]),
|
|
/* 23 */ Corner([21,32,33],[]),
|
|
/* 24 */ Corner([33,35,37],[]),
|
|
/* 25 */ Corner([22,35,36],[5]),
|
|
/* 26 */ Corner([36,38],[5,6]),
|
|
/* 27 */ Corner([25,39],[15,14]),
|
|
/* 28 */ Corner([39,40,42],[14]),
|
|
/* 29 */ Corner([28,40,41],[]),
|
|
/* 30 */
|
|
/* 30 */ Corner([41,43,45],[]),
|
|
/* 31 */ Corner([31,43,44],[]),
|
|
/* 32 */ Corner([44,46,48],[]),
|
|
/* 33 */ Corner([34,46,47],[]),
|
|
/* 34 */ Corner([47,49,51],[]),
|
|
/* 35 */ Corner([37,49,50],[]),
|
|
/* 36 */ Corner([50,53,52],[7]),
|
|
/* 37 */ Corner([38,53],[6,7]),
|
|
/* 38 */ Corner([42,54],[14,13]),
|
|
/* 39 */ Corner([54,55,57],[13]),
|
|
/* 40 */
|
|
/* 40 */ Corner([45,55,56],[]),
|
|
/* 41 */ Corner([56,58,60],[]),
|
|
/* 42 */ Corner([48,58,59],[]),
|
|
/* 43 */ Corner([59,61,63],[]),
|
|
/* 44 */ Corner([51,61,62],[]),
|
|
/* 45 */ Corner([62,65,64],[8]),
|
|
/* 46 */ Corner([52,65],[7,8]),
|
|
/* 47 */ Corner([57,66],[13,12]),
|
|
/* 48 */ Corner([67,66],[12,11]),
|
|
/* 49 */ Corner([60,67,68],[11]),
|
|
/* 50 */
|
|
/* 50 */ Corner([68,69],[11,10]),
|
|
/* 51 */ Corner([69,70,63],[10]),
|
|
/* 52 */ Corner([70,71],[10,9]),
|
|
/* 53 */ Corner([64,71],[8,9]),
|
|
]
|
|
}
|
|
|
|
const assetData = {
|
|
tiles: [
|
|
{ type: "desert", card: 0 },
|
|
{ type: "wood", card: 0 },
|
|
{ type: "wood", card: 1 },
|
|
{ type: "wood", card: 2 },
|
|
{ type: "wood", card: 3 },
|
|
{ type: "wheat", card: 0 },
|
|
{ type: "wheat", card: 1 },
|
|
{ type: "wheat", card: 2 },
|
|
{ type: "wheat", card: 3 },
|
|
{ type: "stone", card: 0 },
|
|
{ type: "stone", card: 1 },
|
|
{ type: "stone", card: 2 },
|
|
{ type: "sheep", card: 0 },
|
|
{ type: "sheep", card: 1 },
|
|
{ type: "sheep", card: 2 },
|
|
{ type: "sheep", card: 3 },
|
|
{ type: "brick", card: 0 },
|
|
{ type: "brick", card: 1 },
|
|
{ type: "brick", card: 2 }
|
|
],
|
|
pips: [
|
|
{ roll: 7, pips: 0 },
|
|
{ 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 }
|
|
],
|
|
borders: [
|
|
{ left: "sheep", right: "bank" },
|
|
{ center: "sheep" },
|
|
{ left: "wheat", right: "bank" },
|
|
{ center: "wood" },
|
|
{ left: "sheep", right: "bank" },
|
|
{ center: "bank" }
|
|
],
|
|
developmentCards: []
|
|
};
|
|
for (let i = 0; i < 14; i++) {
|
|
assetData.developmentCards.push("knight");
|
|
}
|
|
for (let i = 0; i < 6; i++) {
|
|
assetData.developmentCards.push("progress");
|
|
}
|
|
for (let i = 0; i < 5; i++) {
|
|
assetData.developmentCards.push("victoryPoint");
|
|
}
|
|
|
|
const games = {};
|
|
|
|
const processTies = (players) => {
|
|
players.sort((A, B) => {
|
|
if (A.order === B.order) {
|
|
return B.orderRoll - A.orderRoll;
|
|
}
|
|
return A.order - B.order;
|
|
});
|
|
|
|
/* Sort the players into buckets based on their
|
|
* order, and their current roll. If a resulting
|
|
* roll array has more than one element, then there
|
|
* is a tie that must be resolved */
|
|
|
|
let slots = [];
|
|
players.forEach(player => {
|
|
if (!slots[player.order]) {
|
|
slots[player.order] = [];
|
|
}
|
|
if (!(player.orderRoll in slots[player.order])) {
|
|
slots[player.order][player.orderRoll] = [];
|
|
}
|
|
slots[player.order][player.orderRoll].push(player);
|
|
});
|
|
|
|
let ties = false, order = 0;
|
|
slots.forEach((slot) => {
|
|
slot.forEach(pips => {
|
|
if (pips.length !== 1) {
|
|
ties = true;
|
|
pips.forEach(player => {
|
|
player.orderRoll = 0;
|
|
player.order = order;
|
|
player.orderStatus = `Tied for ${order+1}.`;
|
|
});
|
|
} else {
|
|
pips[0].order = order;
|
|
pips[0].orderStatus = `Placed in ${order+1}.`;
|
|
}
|
|
order += pips.length
|
|
})
|
|
});
|
|
|
|
return !ties;
|
|
}
|
|
|
|
|
|
const getPlayerName = (game, player) => {
|
|
for (let id in game.sessions) {
|
|
if (game.sessions[id].player === player) {
|
|
return game.sessions[id].name;
|
|
}
|
|
}
|
|
return '';
|
|
};
|
|
|
|
const getPlayerColor = (game, player) => {
|
|
for (let color in game.players) {
|
|
if (game.players[color] === player) {
|
|
return color;
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
const processGameOrder = (game, player, dice) => {
|
|
let message;
|
|
|
|
player.orderRoll = dice;
|
|
|
|
let players = [];
|
|
|
|
let doneRolling = true;
|
|
|
|
for (let key in game.players) {
|
|
const tmp = game.players[key];
|
|
if (tmp.status === 'Not active') {
|
|
continue;
|
|
}
|
|
if (!tmp.orderRoll) {
|
|
doneRolling = false;
|
|
}
|
|
|
|
players.push(tmp);
|
|
}
|
|
|
|
/* If 'doneRolling' is TRUE then everyone has rolled */
|
|
if (doneRolling) {
|
|
if (processTies(players)) {
|
|
message = `Player order set to ${players.map((player, index) => {
|
|
return `${index+1}. ${getPlayerName(game, player)}`;
|
|
}).join(', ')}.`;
|
|
addChatMessage(game, null, message);
|
|
game.playerOrder = players.map(player => getPlayerColor(game, player));
|
|
game.state = 'active'
|
|
message = `Game has started!`;
|
|
game.turn = getPlayerName(game, players[0]);
|
|
addChatMessage(game, null, message);
|
|
message = `It is ${game.turn}'s turn.`;
|
|
} else {
|
|
message = `There are still ties for player order!`;
|
|
}
|
|
}
|
|
|
|
if (message) {
|
|
addChatMessage(game, null, message);
|
|
}
|
|
}
|
|
|
|
const roll = (game, session) => {
|
|
let message, error;
|
|
|
|
const player = session.player,
|
|
name = session.name ? session.name : "Unnamed";
|
|
|
|
switch (game.state) {
|
|
case "lobby":
|
|
error = `Rolling dice in the lobby is not allowed!`;
|
|
|
|
case "game-order":
|
|
if (!player) {
|
|
error = `This player is not active!`;
|
|
break;
|
|
}
|
|
|
|
if (player.order || player.orderRoll) {
|
|
error = `Player ${name} has already rolled for player order.`;
|
|
break;
|
|
}
|
|
|
|
game.dice = [ Math.ceil(Math.random() * 6) ];
|
|
message = `${name} rolled ${game.dice[0]}.`;
|
|
addChatMessage(game, session, message);
|
|
message = undefined;
|
|
processGameOrder(game, player, game.dice[0]);
|
|
break;
|
|
|
|
case "active":
|
|
game.dice = [ Math.ceil(Math.random() * 6), Math.ceil(Math.random() * 6) ];
|
|
message = `${name} rolled ${game.dice[0]}, ${game.dice[1]}.`;
|
|
break;
|
|
|
|
default:
|
|
error = `Invalid game state (${game.state}) in roll.`;
|
|
break;
|
|
}
|
|
|
|
if (!error && message) {
|
|
addChatMessage(game, session, message);
|
|
}
|
|
return error;
|
|
};
|
|
|
|
const getPlayer = (game, color) => {
|
|
if (!game) {
|
|
return {
|
|
roads: 15,
|
|
cities: 4,
|
|
settlements: 5,
|
|
points: 0,
|
|
status: "Not active",
|
|
lastActive: 0,
|
|
order: 0
|
|
};
|
|
}
|
|
|
|
return game.players[color];
|
|
};
|
|
|
|
const getSession = (game, session) => {
|
|
if (!game.sessions) {
|
|
game.sessions = {};
|
|
}
|
|
|
|
if (!session.player_id) {
|
|
session.player_id = crypto.randomBytes(32).toString('hex');
|
|
}
|
|
|
|
const id = session.player_id;
|
|
|
|
/* If this session is not yet in the game,
|
|
* add it and set the player's name */
|
|
if (!(id in game.sessions)) {
|
|
game.sessions[id] = {
|
|
name: undefined,
|
|
color: undefined,
|
|
player: undefined
|
|
};
|
|
}
|
|
|
|
return game.sessions[id];
|
|
};
|
|
|
|
const loadGame = async (id) => {
|
|
if (/^\.|\//.exec(id)) {
|
|
return undefined;
|
|
}
|
|
|
|
if (id in games) {
|
|
return games[id];
|
|
}
|
|
|
|
let game = await readFile(`games/${id}`)
|
|
.catch(() => {
|
|
return;
|
|
});
|
|
|
|
if (!game) {
|
|
game = createGame(id);
|
|
} else {
|
|
try {
|
|
game = JSON.parse(game);
|
|
} catch (error) {
|
|
console.error(error, game);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (!game.pipOrder || !game.borderOrder || !game.tileOrder) {
|
|
console.log("Shuffling old save file");
|
|
shuffleBoard(game);
|
|
}
|
|
|
|
if (!game.pips || !game.borders || !game.tiles) {
|
|
[ "pips", "borders", "tiles" ].forEach((field) => {
|
|
game[field] = assetData[field]
|
|
});
|
|
}
|
|
|
|
/* Reconnect session player colors to the player objects */
|
|
for (let id in game.sessions) {
|
|
const session = game.sessions[id];
|
|
if (session.color && session.color in game.players) {
|
|
session.player = game.players[session.color];
|
|
} else {
|
|
session.color = undefined;
|
|
session.player = undefined;
|
|
}
|
|
}
|
|
|
|
games[id] = game;
|
|
return game;
|
|
};
|
|
|
|
const clearPlayer = (player) => {
|
|
player.status = 'Not active';
|
|
player.lastActive = 0;
|
|
player.order = 0;
|
|
delete player.orderRoll;
|
|
delete player.orderStatus;
|
|
}
|
|
|
|
const adminActions = (game, action, value) => {
|
|
let color, player;
|
|
|
|
switch (action) {
|
|
case "state":
|
|
switch (value) {
|
|
case 'game-order':
|
|
for (let key in game.players) {
|
|
game.players[key].order = 0;
|
|
delete game.players[key].orderRoll;
|
|
delete game.players[key].orderStatus;
|
|
}
|
|
delete game.turn;
|
|
game.state = 'game-order';
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case "roll":
|
|
let dice = value.replace(/.*-/, '');
|
|
switch (value.replace(/-.*/, '')) {
|
|
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.replace(/-.*/, '')}`
|
|
}
|
|
|
|
addChatMessage(game, null, `Admin rolled ${dice} for ${color}.`);
|
|
player = game.players[color];
|
|
processGameOrder(game, player, dice);
|
|
break;
|
|
|
|
case "kick":
|
|
switch (value) {
|
|
case 'orange': color = 'O'; break;
|
|
case 'red': color = 'R'; break;
|
|
case 'blue': color = 'B'; break;
|
|
case 'white': color = 'W'; break;
|
|
}
|
|
if (!color) {
|
|
return `Unable to find player ${value}`
|
|
}
|
|
|
|
player = game.players[color];
|
|
for (let id in game.sessions) {
|
|
const session = game.sessions[id];
|
|
if (session.player !== player) {
|
|
continue;
|
|
}
|
|
console.log(`Kicking ${value} from ${game.id}.`);
|
|
const preamble = session.name ? `${session.name}, playing as ${color},` : color;
|
|
addChatMessage(game, null, `${preamble} was kicked from game by the Admin.`);
|
|
if (player) {
|
|
session.player = undefined;
|
|
clearPlayer(player);
|
|
}
|
|
session.color = undefined;
|
|
return;
|
|
}
|
|
return `Unable to find active session for ${color} (${value})`;
|
|
|
|
default:
|
|
return `Invalid admin action ${action}.`;
|
|
}
|
|
};
|
|
|
|
const setPlayerName = (game, session, name) => {
|
|
if (session.color) {
|
|
return `You cannot change your name while you are in game.`;
|
|
}
|
|
|
|
/* Check to ensure name is not already in use */
|
|
if (game && name) for (let key in game.sessions) {
|
|
const tmp = game.sessions[key];
|
|
if (tmp.name && tmp.name.toLowerCase() === name.toLowerCase()) {
|
|
return `${name} is already taken.`;
|
|
}
|
|
}
|
|
|
|
const old = session.name;
|
|
let message;
|
|
|
|
session.name = name;
|
|
|
|
if (name) {
|
|
if (!old) {
|
|
message = `A new player has entered the lobby as ${name}.`;
|
|
} else {
|
|
message = `${old} has changed their name to ${name}.`;
|
|
}
|
|
} else {
|
|
return `You can not set your name to nothing!`;
|
|
}
|
|
|
|
addChatMessage(game, null, message);
|
|
|
|
}
|
|
|
|
const setPlayerColor = (game, session, color) => {
|
|
if (!game) {
|
|
return `No game found`;
|
|
}
|
|
|
|
const name = session.name, player = session.player;
|
|
|
|
/* Selecting the same color is a NO-OP */
|
|
if (session.color === color) {
|
|
return;
|
|
}
|
|
|
|
const priorActive = getActiveCount(game);
|
|
let message;
|
|
|
|
if (player) {
|
|
/* Deselect currently active player for this session */
|
|
clearPlayer(player);
|
|
if (game.state !== 'lobby') {
|
|
message = `${name} has exited to the lobby and is no longer playing as ${session.color}.`
|
|
addChatMessage(game, null, message);
|
|
} else {
|
|
message = `${name} is no longer ${session.color}.`;
|
|
}
|
|
session.player = undefined;
|
|
session.color = undefined;
|
|
}
|
|
|
|
/* Verify the player has a name set */
|
|
if (!name) {
|
|
return `You may only select a player when you have set your name.`;
|
|
}
|
|
|
|
/* If the player is not selecting a color, then return */
|
|
if (!color) {
|
|
if (message) {
|
|
addChatMessage(game, null, message);
|
|
}
|
|
return;
|
|
}
|
|
|
|
/* Verify selection is valid */
|
|
if (!(color in game.players)) {
|
|
return `An invalid player selection was attempted.`;
|
|
}
|
|
|
|
/* Verify selection is not already taken */
|
|
for (let key in game.sessions) {
|
|
const tmp = game.sessions[key].player;
|
|
if (tmp && tmp.color === color) {
|
|
return `${game.sessions[key].name} already has ${color}`;
|
|
}
|
|
}
|
|
|
|
/* All good -- set this player to requested selection */
|
|
session.player = getPlayer(game, color);
|
|
|
|
session.player.status = `Active`;
|
|
session.player.lastActive = Date.now();
|
|
session.color = color;
|
|
addChatMessage(game, session, `${session.name} has chosen to play as ${color}.`);
|
|
|
|
const afterActive = getActiveCount(game);
|
|
if (afterActive !== priorActive) {
|
|
if (priorActive < 2 && afterActive >= 2) {
|
|
addChatMessage(game, null,
|
|
`There are now enough players to start the game when you are ready.`);
|
|
}
|
|
}
|
|
};
|
|
|
|
const addChatMessage = (game, session, message) => {
|
|
game.chat.push({
|
|
from: session ? session.name : undefined,
|
|
color: session ? session.color : undefined,
|
|
date: Date.now(),
|
|
message: message
|
|
});
|
|
};
|
|
|
|
const getNextPlayer = (game, name) => {
|
|
let color;
|
|
for (let id in game.sessions) {
|
|
if (game.sessions[id].name === name) {
|
|
color = game.sessions[id].color;
|
|
break;
|
|
}
|
|
}
|
|
if (!color) {
|
|
return name;
|
|
}
|
|
let index = game.playerOrder.indexOf(color);
|
|
index = (index + 1) % game.playerOrder.length;
|
|
for (let id in game.sessions) {
|
|
if (game.sessions[id].color === game.playerOrder[index]) {
|
|
return game.sessions[id].name;
|
|
}
|
|
}
|
|
return name;
|
|
}
|
|
|
|
router.put("/:id/:action/:value?", async (req, res) => {
|
|
const { action, id } = req.params,
|
|
value = req.params.value ? req.params.value : "";
|
|
console.log(`PUT games/${id}/${action}/${value}`);
|
|
|
|
const game = await loadGame(id);
|
|
if (!game) {
|
|
const error = `Game not found and cannot be created: ${id}`;
|
|
return res.status(404).send(error);
|
|
}
|
|
|
|
let error;
|
|
|
|
if ('private-token' in req.headers) {
|
|
if (req.headers['private-token'] !== req.app.get('admin')) {
|
|
error = `Invalid admin credentials.`;
|
|
} else {
|
|
error = adminActions(game, action, value);
|
|
}
|
|
return sendGame(req, res, game, error);
|
|
}
|
|
|
|
const session = getSession(game, req.session);
|
|
|
|
switch (action) {
|
|
case 'player-name':
|
|
error = setPlayerName(game, session, value);
|
|
return sendGame(req, res, game, error);
|
|
case 'player-selected':
|
|
error = setPlayerColor(game, session, value);
|
|
return sendGame(req, res, game, error);
|
|
case 'chat':
|
|
const chat = req.body;
|
|
addChatMessage(game, session, chat.message);
|
|
return sendGame(req, res, game);
|
|
}
|
|
|
|
if (!session.player) {
|
|
error = `Player must have an active color.`;
|
|
return sendGame(req, res, game, error);
|
|
}
|
|
|
|
const name = session.name;
|
|
let message;
|
|
|
|
switch (action) {
|
|
case "roll":
|
|
error = roll(game, session);
|
|
break;
|
|
case "shuffle":
|
|
if (game.state !== "lobby") {
|
|
error = `Game no longer in lobby (${game.state}). Can not shuffle board.`;
|
|
}
|
|
if (!error && game.turns > 0) {
|
|
error = `Game already in progress (${game.turns} so far!) and cannot be shuffled.`;
|
|
}
|
|
if (!error) {
|
|
shuffleBoard(game);
|
|
const message = `${name} requested a new board.`;
|
|
addChatMessage(game, null, message);
|
|
console.log(message);
|
|
}
|
|
break;
|
|
case 'pass':
|
|
if (game.turn !== name) {
|
|
error = `You cannot pass when it isn't your turn.`
|
|
}
|
|
if (!error) {
|
|
game.turn = getNextPlayer(game, name);
|
|
addChatMessage(game, session, `${name} passed their turn.`);
|
|
addChatMessage(game, null, `It is ${game.turn}'s turn.`);
|
|
}
|
|
case "state":
|
|
const state = value;
|
|
if (!state) {
|
|
error = `Invalid state.`;
|
|
break;
|
|
}
|
|
|
|
if (state === game.state) {
|
|
break;
|
|
}
|
|
|
|
switch (state) {
|
|
case "game-order":
|
|
if (game.state !== 'lobby') {
|
|
error = `You cannot start a game from other than the lobby.`;
|
|
break;
|
|
}
|
|
|
|
for (let key in game.players) {
|
|
game.players[key].order = 0;
|
|
delete game.players[key].orderRoll;
|
|
delete game.players[key].orderStatus;
|
|
}
|
|
|
|
message = `${name} requested to start the game.`;
|
|
addChatMessage(game, null, message);
|
|
game.state = state;
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return sendGame(req, res, game, error);
|
|
})
|
|
|
|
router.get("/:id", async (req, res/*, next*/) => {
|
|
const { id } = req.params;
|
|
// console.log("GET games/" + id);
|
|
|
|
let game = await loadGame(id);
|
|
if (game) {
|
|
return sendGame(req, res, game)
|
|
}
|
|
|
|
game = createGame(id);
|
|
|
|
return sendGame(req, res, game);
|
|
});
|
|
|
|
const getActiveCount = (game) => {
|
|
let active = 0;
|
|
for (let color in game.players) {
|
|
const player = game.players[color];
|
|
active += ((player.status && player.status != 'Not active') ? 1 : 0);
|
|
}
|
|
return active;
|
|
}
|
|
|
|
const sendGame = async (req, res, game, error) => {
|
|
const active = getActiveCount(game);
|
|
|
|
/* Enforce game limit of >= 2 players */
|
|
if (active < 2 && game.state != 'lobby' && game.state != 'invalid') {
|
|
let message = "Insufficient players in game. Setting back to lobby."
|
|
console.log(game);
|
|
addChatMessage(game, null, message);
|
|
console.log(message);
|
|
/* It is no one's turn in the lobby */
|
|
delete game.turn;
|
|
game.state = 'lobby';
|
|
}
|
|
game.active = active;
|
|
|
|
/* Update the session lastActive clock */
|
|
let session;
|
|
if (req.session) {
|
|
session = getSession(game, req.session);
|
|
session.lastActive = Date.now();
|
|
if (session.player) {
|
|
session.player.lastActive = session.lastActive;
|
|
}
|
|
} else {
|
|
session = {
|
|
name: "command line"
|
|
};
|
|
}
|
|
|
|
/* Ensure chat messages have a unique date: stamp as it is used as the index key */
|
|
let lastTime = 0;
|
|
if (game.chat) game.chat.forEach((message) => {
|
|
if (message.date <= lastTime) {
|
|
message.date = lastTime + 1;
|
|
}
|
|
lastTime = message.date;
|
|
});
|
|
|
|
/* Shallow copy game, filling its sessions with a shallow copy of sessions so we can then
|
|
* delete the player field from them */
|
|
const reducedGame = Object.assign({}, game, { sessions: {} }),
|
|
reducedSessions = [];
|
|
|
|
for (let id in game.sessions) {
|
|
const reduced = Object.assign({}, game.sessions[id]);
|
|
if (reduced.player) {
|
|
delete reduced.player;
|
|
}
|
|
reducedGame.sessions[id] = reduced;
|
|
|
|
/* Do not send session-id as those are secrets */
|
|
reducedSessions.push(reduced);
|
|
}
|
|
|
|
await writeFile(`games/${game.id}`, JSON.stringify(reducedGame, null, 2))
|
|
.catch((error) => {
|
|
console.error(`Unable to write to games/${game.id}`);
|
|
console.error(error);
|
|
});
|
|
|
|
const playerGame = Object.assign({}, reducedGame, {
|
|
timestamp: Date.now(),
|
|
status: error ? error : "success",
|
|
name: session.name,
|
|
color: session.color,
|
|
order: (session.color in game.players) ? game.players[session.color].order : 0,
|
|
sessions: reducedSessions,
|
|
layout: layout
|
|
});
|
|
|
|
return res.status(200).send(playerGame);
|
|
}
|
|
|
|
const createGame = (id) => {
|
|
/* Look for a new game with random words that does not already exist */
|
|
while (!id) {
|
|
id = randomWords(4).join('_');
|
|
console.log(`Looking for ${id}`);
|
|
try {
|
|
/* If file can be read, it already exists so look for a new name */
|
|
accessSync(`games/${id}`, fs.F_OK);
|
|
id = '';
|
|
} catch (error) {
|
|
console.log(error);
|
|
break;
|
|
}
|
|
}
|
|
|
|
const game = {
|
|
startTime: Date.now(),
|
|
turns: 0,
|
|
state: "lobby", /* lobby, active, finished */
|
|
tokens: [],
|
|
players: {
|
|
R: getPlayer(),
|
|
O: getPlayer(),
|
|
B: getPlayer(),
|
|
W: getPlayer()
|
|
},
|
|
developmentCards: assetData.developmentCards.slice(),
|
|
dice: [ 0, 0 ],
|
|
sheep: 19,
|
|
ore: 19,
|
|
wool: 19,
|
|
brick: 19,
|
|
wheat: 19,
|
|
longestRoad: null,
|
|
largestArmy: null,
|
|
chat: [],
|
|
id: id
|
|
};
|
|
|
|
addChatMessage(game, null, `New game started for ${id}`);
|
|
|
|
[ "pips", "borders", "tiles" ].forEach((field) => {
|
|
game[field] = assetData[field]
|
|
});
|
|
|
|
games[game.id] = game;
|
|
shuffleBoard(game);
|
|
console.log(`New game created: ${game.id}`);
|
|
return game;
|
|
};
|
|
|
|
router.post("/:id?", (req, res/*, next*/) => {
|
|
console.log("POST games/");
|
|
const { id } = req.params;
|
|
if (id && id in games) {
|
|
const error = `Can not create new game for ${id} -- it already exists.`
|
|
console.error(error);
|
|
return res.status(400).send(error);
|
|
}
|
|
|
|
const game = createGame(id);
|
|
|
|
return sendGame(req, res, game);
|
|
});
|
|
|
|
const shuffleBoard = (game) => {
|
|
const seq = [];
|
|
for (let i = 0; i < 6; i++) {
|
|
seq.push(i);
|
|
}
|
|
shuffle(seq);
|
|
game.borderOrder = seq.slice();
|
|
for (let i = 6; i < 19; i++) {
|
|
seq.push(i);
|
|
}
|
|
shuffle(seq);
|
|
game.tileOrder = seq.slice();
|
|
|
|
/* Pip order is from one of the random corners, then rotate around
|
|
* and skip over the desert (robber) */
|
|
|
|
/* Board:
|
|
* 0 1 2
|
|
* 3 4 5 6
|
|
* 7 8 9 10 11
|
|
* 12 13 14 15
|
|
* 16 17 18
|
|
*/
|
|
const order = [
|
|
[ 0, 1, 2, 6, 11, 15, 18, 17, 16, 12, 7, 3, 4, 5, 10, 14, 13, 8, 9 ],
|
|
[ 2, 6, 11, 15, 18, 17, 16, 12, 7, 3, 0, 1, 5, 10, 14, 13, 8, 4, 9 ],
|
|
[ 11, 15, 18, 17, 16, 12, 7, 3, 0, 1, 2, 6, 10, 14, 13, 8, 4, 5, 9 ],
|
|
[ 18, 17, 16, 12, 7, 3, 0, 1, 2, 6, 11, 15, 14, 13, 8, 4, 5, 10, 9 ],
|
|
[ 16, 12, 7, 3, 0, 1, 2, 6, 11, 15, 18, 17, 13, 8, 4, 5, 10, 14, 9 ],
|
|
[ 7, 3, 0, 1, 2, 6, 11, 15, 18, 17, 16, 12, 8, 4, 5, 10, 14, 13, 9 ]
|
|
]
|
|
const sequence = order[Math.floor(Math.random() * order.length)];
|
|
game.pipOrder = [];
|
|
for (let i = 0, p = 0; i < sequence.length; i++) {
|
|
const target = sequence[i];
|
|
/* If the target tile is the desert (18), then set the
|
|
* pip value to the robber (18) otherwise set
|
|
* the target pip value to the currently incremeneting
|
|
* pip value. */
|
|
if (game.tiles[game.tileOrder[target]].type === 'desert') {
|
|
game.pipOrder[target] = 18;
|
|
} else {
|
|
game.pipOrder[target] = p++;
|
|
}
|
|
}
|
|
|
|
shuffle(game.developmentCards)
|
|
}
|
|
|
|
/*
|
|
return gameDB.sequelize.query("SELECT " +
|
|
"photos.*,albums.path AS path,photohashes.hash,modified,(albums.path || photos.filename) AS filepath FROM photos " +
|
|
"LEFT JOIN albums ON albums.id=photos.albumId " +
|
|
"LEFT JOIN photohashes ON photohashes.photoId=photos.id " +
|
|
"WHERE photos.id=:id", {
|
|
replacements: {
|
|
id: id
|
|
}, type: gameDB.Sequelize.QueryTypes.SELECT,
|
|
raw: true
|
|
}).then(function(photos) {
|
|
if (photos.length == 0) {
|
|
return null;
|
|
}
|
|
*/
|
|
if (0) {
|
|
router.get("/*", (req, res/*, next*/) => {
|
|
return gameDB.sequelize.query(query, {
|
|
replacements: replacements, type: gameDB.Sequelize.QueryTypes.SELECT
|
|
}).then((photos) => {
|
|
});
|
|
});
|
|
}
|
|
|
|
module.exports = router;
|