1
0
James Ketrenos 7e7b016a70 Add a message when the admin rolls for a player
Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
2022-02-04 16:55:16 -08:00

896 lines
22 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;
}
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
});
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;