diff --git a/client/src/Board.js b/client/src/Board.js index 580e244..a3134c0 100755 --- a/client/src/Board.js +++ b/client/src/Board.js @@ -209,9 +209,7 @@ const Chat = ({ board, promoteGameState }) => { console.log(`Send: ${event.target.value}`); promoteGameState({ chat: { - player: board.game.activePlayer ? - board.game.activePlayer : - undefined, + player: board.game.color ? board.game.color : undefined, message: event.target.value } }); @@ -246,7 +244,7 @@ const Chat = ({ board, promoteGameState }) => { { messages } )} variant="outlined"/> @@ -283,7 +281,7 @@ const Action = ({ board }) => { } const PlayerName = ({board}) => { - const [name, setName] = useState((board && board.game && board.game.activePlayerName) ? board.game.activePlayerName : ""); + const [name, setName] = useState((board && board.game && board.game.name) ? board.game.name : ""); const nameChange = (event) => { setName(event.target.value); @@ -292,7 +290,7 @@ const PlayerName = ({board}) => { const nameKeyPress = (event) => { if (event.key === "Enter") { console.log(`Send: ${name}`); - if (name != board.game.activePlayerName) { + if (name != board.game.name) { board.setPlayerName(name); } } @@ -312,38 +310,48 @@ const PlayerName = ({board}) => { ); }; +const getPlayerName = (sessions, color) => { + for (let i = 0; i < sessions.length; i++) { + const session = sessions[i]; + if (session.color === color) { + return session.name; + } + } + return null; +} + /* This needs to take in a mechanism to declare the * player's active item in the game */ const Players = ({ board }) => { - const selected = board.game && board.game.activePlayer ? board.game.activePlayer : ""; - const toggleSelected = (key) => { - board.setSelected(selected === key ? "" : key); + board.setSelected(board.game.color === key ? "" : key); } const classes = useStyles(); const players = []; - for (let key in board.game.players) { - const item = board.game.players[key]; + console.log(`Player ${board.game.name} is ${board.game.color}`); + for (let color in board.game.players) { + const item = board.game.players[color]; if (board.game.state !== "lobby" && item.status === 'Not active') { continue; } + const name = getPlayerName(board.game.sessions, color); item.lastActive = item.lastActive > Date.now() ? Date.now() : item.lastActive; players.push(( - + - {key} + {color} - { item.status + ' ' } { item.status != 'Not active' && } )} /> - { (item.status === 'Not active' || selected === key) && + { (item.status === 'Not active' || board.game.color === color) && toggleSelected(key)}/> + checked={board.game.color === color} + onChange={() => toggleSelected(color)}/> } )); @@ -449,13 +457,7 @@ class Board extends React.Component { return res.json(); }).then((game) => { let message; - if (game.status == 'success') { - if (key) { - message = `Player selected ${key}!`; - } else { - message = `Player deselected ${game.activePlayer}!`; - } - } else { + if (game.status !== 'success') { message = game.status; } @@ -490,13 +492,7 @@ class Board extends React.Component { return res.json(); }).then((game) => { let message; - if (game.status == 'success') { - if (name) { - message = `Player name set to ${name}`; - } else { - message = `Player name cleared.`; - } - } else { + if (game.status !== 'success') { message = game.status; } this.updateGame(game); @@ -561,9 +557,10 @@ class Board extends React.Component { } return res.json(); }).then((game) => { - let message = game.status != "success" ? game.status : "Dice rolled!" - if (game.status != "success") { + let message; + if (game.status !== "success") { game.dice = []; + message = game.status; } this.updateGame(game); this.setState({ game: { ...this.state.game, dice: game.dice }, message: message } ); @@ -1323,10 +1320,10 @@ class Board extends React.Component {
this.cards = el}> { game &&
- {game.activePlayer === "" && } + {!game.color && } - { game.activePlayer !== "" && + { game.color && } { this.state.message != "" &&
{this.state.message}
} diff --git a/server/kick b/server/kick index 51b1218..08857bd 100755 --- a/server/kick +++ b/server/kick @@ -1,15 +1,15 @@ #!/bin/bash -ADMIN=$(jq .admin config/local.json) +ADMIN=$(jq -r .admin config/local.json) if [[ "${ADMIN}" == "" ]]; then echo "You need to set your { 'admin': 'secret' } in config/local.json" exit 1 fi id=$1 -user=$2 +color=$2 -if [[ "${id}" == "" ]] || [[ "${user}" == "" ]]; then - echo "Usage: kick GAME-ID USER-NAME" +if [[ "${id}" == "" ]] || [[ "${color}" == "" ]]; then + echo "Usage: kick GAME-ID red|white|blue|orange" exit 1 fi @@ -17,6 +17,6 @@ curl --noproxy '*' -s -L \ --request PUT \ --header "PRIVATE-TOKEN: ${ADMIN}" \ --header "Content-Type: application/json" \ - http://localhost:8930/ketr.ketran/api/v1/games/${id}/kick/${user} | - jq .status + http://localhost:8930/ketr.ketran/api/v1/games/${id}/kick/${color} | + jq -r .status diff --git a/server/routes/games.js b/server/routes/games.js index 727dd20..de01fc2 100755 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -1,11 +1,7 @@ "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; @@ -100,34 +96,68 @@ for (let i = 0; i < 5; i++) { const games = {}; -const roll = (game, player) => { - let error; - if (!player) { - error = "No player active; roll has no action"; - console.log(error); - return error; - } +const roll = (game, session) => { + let message; - const name = player.name; + const player = session.player, + name = session.name ? session.name : "Unnamed"; 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; + case "lobby": + return `Rolling dice in the lobby is not allowed!`; + + case "game-order": + if (player.order) { + retrun = `Player ${name} already rolled for order.`; + } + player.order = game.dice[0]; + message = `${name} rolled ${game.dice[0]} for play order.`; + game.chat.push({ date: Date.now(), message: message }); + break; + + case "in-game": + game.dice = [ Math.ceil(Math.random() * 6), Math.ceil(Math.random() * 6) ]; + message = `${name} rolled ${game.dice[0] + game.dice[1]}.`; + game.chat.push({ date: Date.now(), message: message }); + return; + + default: + return `Invalid game state (${game.state}) in roll.`; + } +}; + +const getPlayer = (game, color) => { + if (!game) { + return { + roads: 15, + cities: 4, + settlements: 5, + points: 0, + status: "Not active", + lastActive: 0, + order: 0 + }; } - error = `Invalid game state (${game.state}) in roll.`; - return error; -} + return game.players[color]; +}; + +const getSession = (game, session) => { + if (!game.sessions) { + game.sessions = {}; + } + + /* If this session is not yet in the game, + * add it and set the player's name */ + if (!(session in game.sessions)) { + game.sessions[session] = { + name: undefined, + player: undefined + }; + } + + return game.sessions[session]; +}; const loadGame = async (id) => { if (/^\.|\//.exec(id)) { @@ -138,27 +168,165 @@ const loadGame = async (id) => { return games[id]; } - const game = await readFile(`games/${id}`) + let game = await readFile(`games/${id}`) .catch(() => { return; }); if (!game) { - games[id] = createGame(id); + game = createGame(id); } else { try { - games[id] = JSON.parse(game); + game = JSON.parse(game); } catch (error) { console.error(error, game); return null; } } - return games[id]; + /* Reconnect session player colors to the player objects */ + for (let id in game.sessions) { + const session = game.sessions[id]; + if (session.color && session.color in game.players) { + session.player = game.players[session.color]; + } else { + session.color = undefined; + session.player = undefined; + } + } + + games[id] = game; + return game; +}; + +const adminActions = (game, action, value) => { + switch (action) { + case "kick": + let color; + 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}` + } + + const 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 name = session.name ? `${session.name} (${color})` : color; + game.chat.push({ + date: Date.now(), + message: `${name} has been kicked from game.` + }); + session.player = 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 ? session.name : "Unknown"; + let message; + + session.name = name; + + if (name) { + message = `${old} is now known as ${name}.`; + } else { + message = `${old} no longer has a name.`; + } + + game.chat.push({ + date: Date.now(), + message: 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; + } + + if (player) { + /* Deselect currently active player for this session */ + player.status = 'Not active'; + player.lastActive = 0; + game.chat.push({ + date: Date.now(), + message: `${session.color} is no longer claimed.` + }); + 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) { + 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; + game.chat.push({ + date: Date.now(), + message: `${color} is now '${session.name}'.` + }); }; router.put("/:id/:action/:value?", async (req, res) => { - const { action, id, value } = req.params; + const { action, id } = req.params, + value = req.params.value ? req.params.value : ""; console.log(`PUT games/${id}/${action}/${value}`); const game = await loadGame(id); @@ -167,159 +335,38 @@ router.put("/:id/:action/:value?", async (req, res) => { 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}`; - } - } + error = adminActions(game, action, value); } + return sendGame(req, res, game, error); + } + + const session = getSession(game, req.session.id); - if (!error) { - const old = req.session.playerName ? req.session.PlayerName : "Unknown"; - let message; - req.session.playerName = name; - if (name) { - message = `${old} is now 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); + 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); } - 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 (!session.player) { + error = `Player must have an active color.`; + return sendGame(req, res, 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; + const name = session.name; switch (action) { case "roll": - error = roll(game, player); + error = roll(game, session); break; case "shuffle": if (game.state !== "lobby") { @@ -336,8 +383,10 @@ router.put("/:id/:action/:value?", async (req, res) => { } break case "state": - const state = req.params.value ? req.params.value : "active"; - if (state != game.state) { + const state = value; + if (!state) { + error = `Invalid state.`; + } else if (state != game.state) { game.state = state; const message = `${name} set game state to ${state}.`; game.chat.push({ date: Date.now(), message: message }); @@ -345,7 +394,7 @@ router.put("/:id/:action/:value?", async (req, res) => { break; } - return sendGame(res, req, game, error); + return sendGame(req, res, game, error); }) router.get("/:id", async (req, res/*, next*/) => { @@ -354,52 +403,48 @@ router.get("/:id", async (req, res/*, next*/) => { let game = await loadGame(id); if (game) { - return sendGame(res, req, game) + return sendGame(req, res, game) } game = createGame(id); - req.session.playerColor = null; - return sendGame(res, req, game); + return sendGame(req, res, game); }); 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; + const { id } = req.params; - 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 { + console.log("PUT games/" + id); + if (!(id in games)) { const error = `Game not found: ${req.params.id}`; return res.status(404).send(error); } + + const game = games[id], + changes = req.body; + + for (let change in changes) { + switch (change) { + 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(req, res, game); }); -const sendGame = async (res, req, game, error) => { +const sendGame = async (req, res, game, error) => { let active = 0; - for (let player in game.players) { - player = game.players[player]; + for (let color in game.players) { + const player = game.players[color]; active += ((player.status && player.status != 'Not active') ? 1 : 0); } if (active < 2 && game.state != 'lobby' && game.state != 'invalid') { @@ -408,26 +453,49 @@ const sendGame = async (res, req, game, error) => { 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(); + + let session; + if (req.session) { + session = getSession(game, req.session.id); + session.lastActive = Date.now(); + } else { + session = { + name: "command line" + }; } - await writeFile(`games/${game.id}`, JSON.stringify(game, null, 2)) + /* 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: {} }); + for (let id in game.sessions) { + const reduced = Object.assign({}, game.sessions[id]); + if (reduced.player) { + delete reduced.player; + } + reducedGame.sessions[id] = 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({}, game, { + + /* Do not send session-id as those are secrets */ + const sessions = []; + for (let id in reducedGame.sessions) { + const reduced = reducedGame.sessions[id]; + sessions.push(reduced); + } + + const playerGame = Object.assign({}, reducedGame, { timestamp: Date.now(), status: error ? error : "success", - activePlayerName: playerName, - activePlayer: playerColor + name: session.name, + color: session.color, + sessions: sessions }); - if (game.id == 'b3c4bd15efe212a2') { -// console.log(req.session); - } + return res.status(200).send(playerGame); } @@ -442,10 +510,10 @@ const createGame = (id) => { 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" } + R: getPlayer(), + O: getPlayer(), + B: getPlayer(), + W: getPlayer() }, developmentCards: assetData.developmentCards.slice(), dice: [ 0, 0 ], @@ -475,9 +543,8 @@ router.post("/:id?", (req, res/*, next*/) => { } const game = createGame(id); - req.session.playerColor = null; - - return sendGame(res, req, game); + + return sendGame(req, res, game); }); const shuffleBoard = (game) => {