523 lines
14 KiB
JavaScript
Executable File
523 lines
14 KiB
JavaScript
Executable File
"use strict";
|
|
|
|
const express = require("express"),
|
|
config = require("config"),
|
|
moment = require("moment"),
|
|
crypto = require("crypto"),
|
|
util = require("util"),
|
|
Promise = require("bluebird"),
|
|
{ readFile, writeFile } = require("fs").promises;
|
|
|
|
let gameDB;
|
|
|
|
require("../db/games").then(function(db) {
|
|
gameDB = db;
|
|
});
|
|
|
|
const router = express.Router();
|
|
|
|
function shuffle(array) {
|
|
var currentIndex = array.length, temporaryValue, randomIndex;
|
|
|
|
// While there remain elements to shuffle...
|
|
while (0 !== currentIndex) {
|
|
|
|
// Pick a remaining element...
|
|
randomIndex = Math.floor(Math.random() * currentIndex);
|
|
currentIndex -= 1;
|
|
|
|
// And swap it with the current element.
|
|
temporaryValue = array[currentIndex];
|
|
array[currentIndex] = array[randomIndex];
|
|
array[randomIndex] = temporaryValue;
|
|
}
|
|
|
|
return array;
|
|
}
|
|
|
|
const assetData = {
|
|
tiles: [
|
|
{ type: "wood", y: 0. / 4. },
|
|
{ type: "wood", y: 1. / 4. },
|
|
{ type: "wood", y: 2. / 4. },
|
|
{ type: "wood", y: 3. / 4. },
|
|
{ type: "wheat", y: 0. / 4. },
|
|
{ type: "wheat", y: 1. / 4. },
|
|
{ type: "wheat", y: 2. / 4. },
|
|
{ type: "wheat", y: 3. / 4. },
|
|
{ type: "stone", y: 0. / 4. },
|
|
{ type: "stone", y: 1. / 4. },
|
|
{ type: "stone", y: 2. / 4. },
|
|
{ type: "sheep", y: 0. / 4. },
|
|
{ type: "sheep", y: 1. / 4. },
|
|
{ type: "sheep", y: 2. / 4. },
|
|
{ type: "sheep", y: 3. / 4. },
|
|
{ type: "brick", y: 0. / 4. },
|
|
{ type: "brick", y: 1. / 4. },
|
|
{ type: "brick", y: 2. / 4. },
|
|
{ type: "robber", y: 0 }
|
|
],
|
|
pips: [
|
|
{ roll: 7, pips: 0, y: 3. / 6., x: 0. / 6. },
|
|
{ roll: 5, pips: 4, y: 0. / 6., x: 0. / 6. },
|
|
{ roll: 2, pips: 1, y: 0. / 6., x: 1. / 6. },
|
|
{ roll: 6, pips: 5, y: 0. / 6., x: 2. / 6. },
|
|
{ roll: 3, pips: 2, y: 0. / 6., x: 3. / 6. },
|
|
{ roll: 8, pips: 5, y: 0. / 6., x: 4. / 6. },
|
|
{ roll: 10, pips: 3, y: 0. / 6., x: 5. / 6. },
|
|
{ roll: 9, pips: 4, y: 1. / 6., x: 0. / 6. },
|
|
{ roll: 12, pips: 1, y: 1. / 6., x: 1. / 6. },
|
|
{ roll: 11, pips: 2, y: 1. / 6., x: 2. / 6. },
|
|
{ roll: 4, pips: 3, y: 1. / 6., x: 3. / 6. },
|
|
{ roll: 8, pips: 5, y: 1. / 6., x: 4. / 6. },
|
|
{ roll: 10, pips: 3, y: 1. / 6., x: 5. / 6. },
|
|
{ roll: 9, pips: 4, y: 2. / 6., x: 0. / 6. },
|
|
{ roll: 4, pips: 3, y: 2. / 6., x: 1. / 6. },
|
|
{ roll: 5, pips: 4, y: 2. / 6., x: 2. / 6. },
|
|
{ roll: 6, pips: 6, y: 2. / 6., x: 3. / 6. },
|
|
{ roll: 3, pips: 2, y: 2. / 6., x: 4. / 6. },
|
|
{ roll: 11, pips: 2, y: 2. / 6., x: 5. / 6. }
|
|
],
|
|
borders: [
|
|
{ file: 'borders-1.6.png', left: "sheep", right: "bank" },
|
|
{ file: 'borders-2.1.png', center: "sheep" },
|
|
{ file: 'borders-3.2.png', left: "wheat", right: "bank" },
|
|
{ file: 'borders-4.3.png', center: "wood" },
|
|
{ file: 'borders-5.4.png', left: "sheep", right: "bank" },
|
|
{ file: 'borders-6.5.png', 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 roll = (game, player) => {
|
|
let error;
|
|
if (!player) {
|
|
error = "No player active; roll has no action";
|
|
console.log(error);
|
|
return error;
|
|
}
|
|
|
|
const name = player.name;
|
|
|
|
switch (game.state) {
|
|
case "lobby":
|
|
if (player.order) {
|
|
error = `Player ${name} already rolled for order.`;
|
|
console.log(error);
|
|
return error;
|
|
}
|
|
game.dice = [ Math.ceil(Math.random() * 6) ];
|
|
player.order = game.dice[0];
|
|
const message = `${name} rolled ${game.dice[0]} for play order.`;
|
|
game.chat.push({ date: Date.now(), message: message });
|
|
console.log(message);
|
|
return;
|
|
}
|
|
|
|
error = `Invalid game state (${game.state}) in roll.`;
|
|
return error;
|
|
}
|
|
|
|
const loadGame = async (id) => {
|
|
if (/^\.|\//.exec(id)) {
|
|
return undefined;
|
|
}
|
|
|
|
if (id in games) {
|
|
return games[id];
|
|
}
|
|
|
|
const game = await readFile(`games/${id}`)
|
|
.catch(() => {
|
|
return;
|
|
});
|
|
|
|
if (!game) {
|
|
return undefined;
|
|
}
|
|
|
|
try {
|
|
games[id] = JSON.parse(game);
|
|
} catch (error) {
|
|
console.error(error, game);
|
|
return null;
|
|
}
|
|
|
|
return games[id];
|
|
};
|
|
|
|
router.put("/:id/:action/:value?", async (req, res) => {
|
|
const { action, id, value } = req.params;
|
|
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);
|
|
}
|
|
|
|
const color = req.session && req.session.playerColor in game.players ?
|
|
req.session.playerColor : undefined;
|
|
|
|
let player;
|
|
let error;
|
|
|
|
if ('private-token' in req.headers) {
|
|
if (req.headers['private-token'] !== req.app.get('admin')) {
|
|
error = `Invalid admin credentials.`;
|
|
}
|
|
|
|
switch (action) {
|
|
case "kick":
|
|
error = `Unable to find player ${value}`
|
|
for (let color in game.players) {
|
|
player = game.players[color];
|
|
if (player.name.toLowerCase() === value.toLowerCase()) {
|
|
console.log(`Kicking ${value} from ${id}.`);
|
|
game.chat.push({
|
|
date: Date.now(),
|
|
message: `${player.name} has been kicked from game.`
|
|
});
|
|
player.session = '';
|
|
player.name = '';
|
|
player.status = 'Not active';
|
|
player.lastActive = 0;
|
|
error = undefined;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
return sendGame(res, req, game, error);
|
|
}
|
|
|
|
if (action == "player-name") {
|
|
const name = value ? value : "";
|
|
if (color) {
|
|
error = `You cannot change your name while you are in game.`;
|
|
} else {
|
|
if (game) for (let key in game.players) {
|
|
player = game.players[key];
|
|
if (player.name && player.name.toLowerCase() === name.toLowerCase()) {
|
|
error = `${name} is already taken by ${key}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!error) {
|
|
const old = req.session.playerName ? req.session.PlayerName : "";
|
|
let message;
|
|
req.session.playerName = name;
|
|
if (name) {
|
|
message = `${old} is no known as ${name}.`;
|
|
} else {
|
|
message = `${old} no longer has a name.`;
|
|
}
|
|
game.chat.push({
|
|
date: Date.now(),
|
|
message: message
|
|
});
|
|
}
|
|
|
|
return sendGame(res, req, game, error);
|
|
}
|
|
|
|
if (action == "player-selected") {
|
|
if (!game) {
|
|
error = `No game found`;
|
|
return sendGame(res, req, game, error);
|
|
}
|
|
|
|
const selected = req.params.value,
|
|
name = req.session.playerName;
|
|
|
|
console.log(`player-selected requested for ${selected} by ${name}`);
|
|
|
|
/* Deselect currently active player for this session */
|
|
for (let key in game.players) {
|
|
if (key === color && selected !== key) {
|
|
player = game.players[key];
|
|
if (player.session === req.session.id) {
|
|
player.session = '';
|
|
player.name = '';
|
|
player.status = 'Not active';
|
|
req.session.playerColor = '';
|
|
game.chat.push({
|
|
date: Date.now(),
|
|
message: `${key} is no longer claimed.`
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Verify the player has a name set */
|
|
if (!name) {
|
|
error = `You may only select a player when you have set your name.`;
|
|
}
|
|
|
|
/* Verify this player's name is not already active in the game */
|
|
if (!error) {
|
|
for (let key in game.players) {
|
|
player = game.players[key];
|
|
if (key === color && selected !== key && player.name == name) {
|
|
error = `This name is already taken by ${key}`;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Verify selection is valid */
|
|
if (!error && !(selected in game.players)) {
|
|
error = `An invalid player selection was attempted.`;
|
|
}
|
|
|
|
/* Verify selection is not already taken */
|
|
if (!error) {
|
|
player = game.players[selected];
|
|
if (player.session) {
|
|
error = `${player.name} already has ${selected}`;
|
|
}
|
|
}
|
|
|
|
/* All good -- set this player to requested selection */
|
|
if (!error) {
|
|
player.session = req.session.id;
|
|
player.name = req.session.playerName;
|
|
player.status = `Active`;
|
|
player.lastActive = Date.now();
|
|
req.session.playerColor = selected;
|
|
game.chat.push({
|
|
date: Date.now(),
|
|
message: `${selected} is now '${player.name}'.`
|
|
});
|
|
}
|
|
|
|
return sendGame(res, req, game, error);
|
|
}
|
|
|
|
if (!req.session.playerColor ||
|
|
!(req.session.playerColor in game.players)) {
|
|
error = `Invalid player: ${req.session.playerColor}`;
|
|
return sendGame(res, req, game, error);
|
|
}
|
|
|
|
player = game.players[req.session.playerColor];
|
|
const name = player.name;
|
|
|
|
switch (action) {
|
|
case "roll":
|
|
error = roll(game, player);
|
|
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.`;
|
|
game.chat.push({ date: Date.now(), message: message });
|
|
console.log(message);
|
|
}
|
|
break
|
|
case "state":
|
|
const state = req.params.value ? req.params.value : "active";
|
|
if (state != game.state) {
|
|
game.state = state;
|
|
const message = `${name} set game state to ${state}.`;
|
|
game.chat.push({ date: Date.now(), message: message });
|
|
}
|
|
break;
|
|
}
|
|
|
|
return sendGame(res, req, game, error);
|
|
})
|
|
|
|
router.get("/:id", async (req, res/*, next*/) => {
|
|
const { id } = req.params;
|
|
console.log("GET games/" + id);
|
|
let error;
|
|
|
|
const game = await loadGame(id);
|
|
if (game) {
|
|
return sendGame(res, req, game)
|
|
}
|
|
|
|
error = `Game ${id} not found -- returning invalid game state.`;
|
|
const invalid = {
|
|
id: id,
|
|
players: {},
|
|
state: 'invalid'
|
|
};
|
|
return sendGame(res, req, invalid, error);
|
|
});
|
|
|
|
router.put("/:id", (req, res/*, next*/) => {
|
|
console.log("PUT games/" + req.params.id);
|
|
if (req.params.id in games) {
|
|
const game = games[req.params.id],
|
|
changes = req.body;
|
|
|
|
console.log(req.session.id, req.session.playerColor);
|
|
console.log(JSON.stringify(changes, null, 2));
|
|
|
|
for (let change in changes) {
|
|
switch (change) {
|
|
case "players":
|
|
break;
|
|
case "chat":
|
|
console.log("Chat change.");
|
|
game.chat.push({
|
|
from: changes.chat.player,
|
|
date: Date.now(),
|
|
message: changes.chat.message
|
|
});
|
|
if (game.chat.length > 10) {
|
|
game.chat.splice(0, game.chat.length - 10);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return sendGame(res, req, game);
|
|
} else {
|
|
const error = `Game not found: ${req.params.id}`;
|
|
return res.status(404).send(error);
|
|
}
|
|
});
|
|
|
|
const sendGame = async (res, req, game, error) => {
|
|
let active = 0;
|
|
for (let player in game.players) {
|
|
player = game.players[player];
|
|
active += ((player.status && player.status != 'Not active') ? 1 : 0);
|
|
}
|
|
if (active < 2 && game.state != 'lobby' && game.state != 'invalid') {
|
|
let message = "Insufficient players in game. Setting back to lobby."
|
|
game.chat.push({ date: Date.now(), message: message });
|
|
console.log(message);
|
|
game.state = 'lobby';
|
|
}
|
|
const playerColor = (req.session && req.session.playerColor) ? req.session.playerColor : "",
|
|
playerName = (req.session && req.session.playerName) ? req.session.playerName : "";
|
|
if (playerColor in game.players) {
|
|
game.players[playerColor].lastActive = Date.now();
|
|
}
|
|
|
|
await writeFile(`games/${game.id}`, JSON.stringify(game, null, 2))
|
|
.catch((error) => {
|
|
console.error(`Unable to write to games/${game.id}`);
|
|
console.error(error);
|
|
});
|
|
const playerGame = Object.assign({}, game, {
|
|
timestamp: Date.now(),
|
|
status: error ? error : "success",
|
|
activePlayerName: playerName,
|
|
activePlayer: playerColor
|
|
});
|
|
if (game.id == 'b3c4bd15efe212a2') {
|
|
// console.log(req.session);
|
|
}
|
|
return res.status(200).send(playerGame);
|
|
}
|
|
|
|
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 = {
|
|
startTime: Date.now(),
|
|
turns: 0,
|
|
state: "lobby", /* lobby, in-game, finished */
|
|
tiles: [],
|
|
pips: [],
|
|
borders: [],
|
|
tokens: [],
|
|
players: {
|
|
R: { roads: 15, cities: 4, settlements: 5, points: 0, name: "", status: "Not active" },
|
|
O: { roads: 15, cities: 4, settlements: 5, points: 0, name: "", status: "Not active" },
|
|
B: { roads: 15, cities: 4, settlements: 5, points: 0, name: "", status: "Not active" },
|
|
W: { roads: 15, cities: 4, settlements: 5, points: 0, name: "", status: "Not active" }
|
|
},
|
|
developmentCards: assetData.developmentCards.slice(),
|
|
dice: [ 0, 0 ],
|
|
sheep: 19,
|
|
ore: 19,
|
|
wool: 19,
|
|
brick: 19,
|
|
wheat: 19,
|
|
longestRoad: null,
|
|
largestArmy: null,
|
|
chat: [ { from: "R", date: Date.now(), message: "Server initialized!" } ],
|
|
id: id ? id : crypto.randomBytes(8).toString('hex')
|
|
};
|
|
|
|
games[game.id] = game;
|
|
req.session.playerColor = null;
|
|
shuffleBoard(game);
|
|
console.log(`New game created: ${game.id}`);
|
|
return sendGame(res, req, game);
|
|
});
|
|
|
|
const shuffleBoard = (game) => {
|
|
[ "tiles", "pips", "borders" ].forEach((field) => {
|
|
game[field] = []
|
|
for (let i = 0; i < assetData[field].length; i++) {
|
|
game[field].push(i);
|
|
}
|
|
/* Shuffle an array of indexes */
|
|
shuffle(game[field]);
|
|
/* Convert from an index array to a full array */
|
|
for (let i = 0; i < assetData[field].length; i++) {
|
|
game[field][i] = assetData[field][game[field][i]];
|
|
}
|
|
});
|
|
|
|
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;
|