diff --git a/client/src/App.css b/client/src/App.css index 817653f..315ce9d 100755 --- a/client/src/App.css +++ b/client/src/App.css @@ -28,7 +28,7 @@ body { left: 0; bottom: 0; right: 0; - background-color: #00000060; + background-color: #80000060; } .Table .ErrorDialog .Error { @@ -36,6 +36,23 @@ body { padding: 1rem; } +.Table .WarningDialog { + z-index: 10000; + display: flex; + justify-content: space-around; + align-items: center; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; +} + +.Table .WarningDialog .Warning { + display: flex; + padding: 1rem; +} + .Table .Game { display: flex; flex-direction: column; diff --git a/client/src/App.js b/client/src/App.js index 38b9c5e..0ed38b6 100755 --- a/client/src/App.js +++ b/client/src/App.js @@ -17,7 +17,8 @@ import { Chat } from "./Chat.js"; import { MediaAgent } from "./MediaControl.js"; import { Board } from "./Board.js"; import { Actions } from "./Actions.js"; -import { base, gamesPath, debounce } from './Common.js'; +import { base, gamesPath } from './Common.js'; +import { GameOrder } from "./GameOrder.js"; import history from "./history.js"; import "./App.css"; @@ -28,66 +29,89 @@ const Table = () => { const [ ws, setWs ] = useState(global.ws); const [ name, setName ] = useState(global.name); const [ error, setError ] = useState(undefined); + const [ warning, setWarning ] = useState(undefined); const [ peers, setPeers ] = useState({}); const [loaded, setLoaded] = useState(false); - const [connecting, setConnecting] = useState(false); + const [connecting, setConnecting] = useState(undefined); + const [state, setState] = useState(undefined); + const [color, setColor] = useState(undefined); + const fields = [ 'name', 'id', 'state', 'color', 'name' ]; + /* useEffect(() => { console.log(peers); }, [peers]); + */ const onWsOpen = (event) => { console.log(`ws: open`); setError(""); - /* We do not set the socket as bound until the 'open' message + /* We do not set the socket as connected until the 'open' message * comes through */ - setWs(event.target); - setConnecting(false); + setConnecting(event.target); /* Request a full game-update * We only need gameId and name for App.js, however in the event * of a network disconnect, we need to refresh the entire game * state on reload so all bound components reflect the latest * state */ - if (loaded) { - event.target.send(JSON.stringify({ - type: 'game-update' - })); - if (name) { - event.target.send(JSON.stringify({ - type: 'player-name', - name - })); - } - } else { - event.target.send(JSON.stringify({ - type: 'get', - fields: [ 'name', 'id' ] - })) - } + event.target.send(JSON.stringify({ + type: 'game-update' + })); + event.target.send(JSON.stringify({ + type: 'get', + fields + })); }; const onWsMessage = (event) => { const data = JSON.parse(event.data); switch (data.type) { case 'error': - console.error(data.error); + console.error(`App - error`, data.error); window.alert(data.error); break; + case 'warning': + setWarning(`App - warning`, data.warning); + setTimeout(() => { + if (data.warning === warning) { + setWarning(""); + } + }, 3000); + break; case 'game-update': if (!loaded) { setLoaded(true); } console.log(`ws: message - ${data.type}`, data.update); + if ('player' in data.update) { + const player = data.update.player; + if (player.name !== name) { + console.log(`App - setting name (via player): ${data.update.name}`); + setName(data.update.name); + } + if (player.color !== color) { + console.log(`App - setting color (via player): ${data.update.color}`); + setColor(data.update.color); + } + } if ('name' in data.update && data.update.name !== name) { - console.log(`Updating name to ${data.update.name}`); + console.log(`App - setting name: ${data.update.name}`); setName(data.update.name); } if ('id' in data.update && data.update.id !== gameId) { - console.log(`Updating id to ${data.update.id}`); + console.log(`App - setting gameId ${data.update.id}`); setGameId(data.update.id); } + if ('state' in data.update && data.update.state !== state) { + console.log(`App - setting game state: ${data.update.state}`); + setState(data.update.state); + } + if ('color' in data.update && data.update.color !== color) { + console.log(`App - setting color: ${color}`); + setColor(data.update.color); + } break; default: break; @@ -98,7 +122,7 @@ const Table = () => { let timer = 0; function reset() { timer = 0; - setConnecting(false); + setConnecting(undefined); }; return _ => { if (timer) { @@ -109,20 +133,19 @@ const Table = () => { })(); const onWsError = (event) => { - console.log(`ws: error`, event); + console.error(`ws: error`, event); const error = `Connection to Ketr Ketran game server failed! ` + `Connection attempt will be retried every 5 seconds.`; - console.error(error); + setError(error); setWs(undefined); resetConnection(); - setError(error); }; const onWsClose = (event) => { const error = `Connection to Ketr Ketran game was lost. ` + `Attempting to reconnect...`; - console.error(error); - console.log(`ws: close`); + console.warn(`ws: close`); + setError(error); setWs(undefined); resetConnection(); }; @@ -152,7 +175,7 @@ const Table = () => { return; } - console.log("Requesting new game."); + console.log(`Requesting new game.`); window.fetch(`${base}/api/v1/games/`, { method: 'POST', @@ -190,8 +213,13 @@ const Table = () => { return; } - let socket = ws; - if (!ws) { + const unbind = () => { + console.log(`table - unbind`); + } + + console.log(`table - bind`); + + if (!ws && !connecting) { let loc = window.location, new_uri; if (loc.protocol === "https:") { new_uri = "wss"; @@ -200,42 +228,68 @@ const Table = () => { } new_uri = `${new_uri}://${loc.host}${base}/api/v1/games/ws/${gameId}`; console.log(`Attempting WebSocket connection to ${new_uri}`); - socket = new WebSocket(new_uri); - setConnecting(true); + setWs(new WebSocket(new_uri)); + setConnecting(undefined); + return unbind; + } + + if (!ws) { + return unbind; } - console.log('table - bind'); const cbOpen = e => refWsOpen.current(e); const cbMessage = e => refWsMessage.current(e); const cbClose = e => refWsClose.current(e); const cbError = e => refWsError.current(e); - socket.addEventListener('open', cbOpen); - socket.addEventListener('close', cbClose); - socket.addEventListener('error', cbError); - socket.addEventListener('message', cbMessage); + ws.addEventListener('open', cbOpen); + ws.addEventListener('close', cbClose); + ws.addEventListener('error', cbError); + ws.addEventListener('message', cbMessage); return () => { - if (socket) { - console.log('table - unbind'); - socket.removeEventListener('open', cbOpen); - socket.removeEventListener('close', cbClose); - socket.removeEventListener('error', cbError); - socket.removeEventListener('message', cbMessage); - } + unbind(); + ws.removeEventListener('open', cbOpen); + ws.removeEventListener('close', cbClose); + ws.removeEventListener('error', cbError); + ws.removeEventListener('message', cbMessage); } }, [ setWs, connecting, setConnecting, gameId, ws, refWsOpen, refWsMessage, refWsClose, refWsError ]); - console.log(`Loaded: ${loaded}`); - - return + return
- { error &&
{ error }
}
- + { error &&
{ error }
} + { warning &&
{ warning }
} + + { color && state === 'game-order' && + + } + + { /* state === 'winner' && + + } + + { state === 'normal' && + turn && turn.actions && turn.actions.indexOf('trade') !== -1 && + + } + + { cardActive && + + } + + { isTurn && turn && turn.actions && game.turn.actions.indexOf('select-resources') !== -1 && + + } + + { game.state === 'normal' && + turn && isTurn && turn.actions && turn.actions.indexOf('steal-resource') !== -1 && + + */ } +
todo: player's hand
@@ -248,7 +302,6 @@ const Table = () => { }; const App = () => { - console.log(`Base: ${base}`); return ( diff --git a/client/src/Board.js b/client/src/Board.js index 167b498..f5f9c84 100644 --- a/client/src/Board.js +++ b/client/src/Board.js @@ -2,6 +2,24 @@ import React, { useEffect, useState, useContext, useRef, useMemo } from "react"; import { assetsPath, debounce } from "./Common.js"; import "./Board.css"; import { GlobalContext } from "./GlobalContext.js"; +const rows = [3, 4, 5, 4, 3, 2]; /* The final row of 2 is to place roads and corners */ + +const + hexRatio = 1.1547, + tileWidth = 67, + tileHalfWidth = tileWidth * 0.5, + tileHeight = tileWidth * hexRatio, + tileHalfHeight = tileHeight * 0.5, + radius = tileHeight * 2, + borderOffsetX = 86, /* ~1/10th border image width... hand tuned */ + borderOffsetY = 3; + +/* Actual sizing */ +const + tileImageWidth = 90, /* Based on hand tuned and image width */ + tileImageHeight = tileImageWidth/hexRatio, + borderImageWidth = (2 + 2/3) * tileImageWidth, /* 2.667 * .Tile.width */ + borderImageHeight = borderImageWidth * 0.29; /* 0.29 * .Border.height */ const Board = () => { const { ws } = useContext(GlobalContext); @@ -12,13 +30,14 @@ const Board = () => { const [cornerElements, setCornerElements] = useState(<>); const [roadElements, setRoadElements] = useState(<>); const [ signature, setSignature ] = useState(""); + const [ generated, setGenerated ] = useState(""); const [ robber, setRobber ] = useState(-1); const [ robberName, setRobberName ] = useState([]); - const [ pips, setPips ] = useState([]); - const [ pipOrder, setPipOrder ] = useState([]); - const [ borders, setBorders ] = useState([]); - const [ borderOrder, setBorderOrder ] = useState([]); - const [ tiles, setTiles ] = useState([]); + const [ pips, setPips ] = useState(); + const [ pipOrder, setPipOrder ] = useState(); + const [ borders, setBorders ] = useState(); + const [ borderOrder, setBorderOrder ] = useState(); + const [ tiles, setTiles ] = useState(); const [ tileOrder, setTileOrder ] = useState([]); const [ placements, setPlacements ] = useState(undefined); const [ turn, setTurn ] = useState({}); @@ -30,60 +49,121 @@ const Board = () => { 'pips', 'pipOrder', 'borders', 'borderOrder', 'tiles', 'tileOrder', 'placements', 'turn', 'state', 'color', 'longestRoadLength' ], []); - + + /* Placements is a structure of roads and corners arrays + * indicating what is stored at each of the locations + * + * Corners consist of a type and color + * Roads consist of a color, and longestRoad + * + * See games.js resetGame, placeRoad, placeSettlement, placeCity, + * and calculateRoadLengths + * + * Returns: true === differences, false === same + */ + const comparePlacements = (A, B) => { + if (!A && !B) { + return false; /* same */ + } + if ((A && !B) + || (!A && B)) { + return true; + } + + if ((A.roads.length !== B.roads.length) + || (A.corners.length !== B.corners.length)) { + return true; + } + + /* Roads compare color and longestRoad */ + for (let i = 0; i < A.roads.length; i++) { + if (A.roads[i].color !== B.roads[i].color) { + return true; + } + if (A.roads[i].longestRoad !== B.roads[i].longestRoad) { + return true; + } + } + + /* Corners compare type and color */ + for (let i = 0; i < A.corners.length; i++) { + if (A.corners[i].type !== B.corners[i].type) { + return true; + } + if (A.corners[i].color !== B.corners[i].color) { + return true; + } + } + + return false; /* same */ + }; + const onWsMessage = (event) => { const data = JSON.parse(event.data); switch (data.type) { case 'game-update': - if (data.update.signature && data.update.signature !== signature) { - setSignature(data.update.signature); - } - - if (data.update.robber && data.update.robber !== robber) { + console.log(`board - game update`, data.update); + if ('robber' in data.update && data.update.robber !== robber) { setRobber(data.update.robber); } - if (data.update.robberName && data.update.robberName !== robberName) { + if ('robberName' in data.update && data.update.robberName !== robberName) { setRobberName(data.update.robberName); } - if (data.update.pips) { - setPips(data.update.pips); - } - if (data.update.pipOrder) { - setPipOrder(data.update.pipOrder); - } - - if (data.update.borders) { - setBorders(data.update.borders); - } - if (data.update.borderOrder) { - setBorderOrder(data.update.borderOrder); - } - - if (data.update.tiles) { - setTiles(data.update.tiles); - } - if (data.update.tileOrder) { - setTileOrder(data.update.tileOrder); - } - - if (data.update.state && data.update.state !== state) { + if ('state' in data.update && data.update.state !== state) { setState(data.update.state); } - if (data.update.color && data.update.color !== color) { + + if ('color' in data.update && data.update.color !== color) { setColor(data.update.color); } - if (data.update.longestRoadLength + + if ('longestRoadLength' in data.update && data.update.longestRoadLength !== longestRoadLength) { setLongestRoadLength(data.update.longestRoadLength); } - if (data.update.turn) { + + if ('turn' in data.update) { setTurn(data.update.turn); } - if (data.update.placements) { - console.log(`placements`, data.update.placements); - setPlacements(data.update.placements); + + if ('placement' in data.update) { + if (comparePlacements(data.update.placements, placements)) { + console.log(`placements`, data.update.placements); + setPlacements(data.update.placements); + } + } + + if ('signature' in data.update && data.update.signature !== signature) { + setSignature(data.update.signature); + + /* The following are only updated if there is a new game + * signature */ + + if ('pipOrder' in data.update) { + setPipOrder(data.update.pipOrder); + } + + if ('borderOrder' in data.update) { + setBorderOrder(data.update.borderOrder); + } + + if ('tileOrder' in data.update) { + setTileOrder(data.update.tileOrder); + } + } + + /* This is permanent static data from the server -- do not update + * once set */ + if ('pips' in data.update && !pips) { + setPips(data.update.pips); + } + if ('tiles' in data.update && !tiles) { + setTiles(data.update.tiles); + } + if ('borders' in data.update && !borders) { + setBorders(data.update.borders); } break; default: @@ -135,9 +215,11 @@ const Board = () => { if (!ws) { return; } + console.log('board - bind'); const cbMessage = e => refWsMessage.current(e); ws.addEventListener('message', cbMessage); return () => { + console.log('board - unbind'); ws.removeEventListener('message', cbMessage); } }, [ws, refWsMessage]); @@ -152,23 +234,6 @@ const Board = () => { })); }, [ws, fields]); - const - hexRatio = 1.1547, - tileWidth = 67, - tileHalfWidth = tileWidth * 0.5, - tileHeight = tileWidth * hexRatio, - tileHalfHeight = tileHeight * 0.5, - radius = tileHeight * 2, - borderOffsetX = 86, /* ~1/10th border image width... hand tuned */ - borderOffsetY = 3; - - /* Actual sizing */ - const - tileImageWidth = 90, /* Based on hand tuned and image width */ - tileImageHeight = tileImageWidth/hexRatio, - borderImageWidth = (2 + 2/3) * tileImageWidth, /* 2.667 * .Tile.width */ - borderImageHeight = borderImageWidth * 0.29; /* 0.29 * .Border.height */ - const Tile = ({tile}) => { const onClick = (event) => { console.log(`Tile clicked: ${tile.index}`); @@ -237,7 +302,7 @@ const Board = () => { sendPlacement('place-robber', pip.index); }; - return
{ if (!signature) { return; } - const rows = [3, 4, 5, 4, 3, 2]; /* The final row of 2 is to place roads and corners */ + if (signature === generated) { + return; + } + + if (!pips || !pipOrder || !borders || !borderOrder + || !tiles || !tileOrder) { + return; + } + + console.log(`board - Generate board - ${signature}`); + const generateRoads = () => { let row = 0, rowCount = 0; let y = -2.5 + tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth, @@ -406,7 +481,6 @@ const Board = () => { return pipOrder.map(order => { pip = { roll: pips[order].roll, - robber: index === robber, index: index++, top: y, left: x, @@ -484,19 +558,18 @@ const Board = () => { }); }; - console.log(`Generate for ${signature}`); setPipElements(generatePips()); setBorderElements(generateBorders()); setTileElements(generateTiles()); setCornerElements(generateCorners()); setRoadElements(generateRoads()); + + setGenerated(signature); }, [ signature, setPipElements, setBorderElements, setTileElements, setCornerElements, setRoadElements, - borderImageWidth, radius, tileHalfHeight, tileHalfWidth, tileHeight, - borderImageHeight, - borderOrder, borders, pipOrder, pips, robber, tileOrder, tiles + borderOrder, borders, pipOrder, pips, tileOrder, tiles ]); if (turn) { diff --git a/client/src/Chat.js b/client/src/Chat.js index 0f95544..d9ae417 100644 --- a/client/src/Chat.js +++ b/client/src/Chat.js @@ -1,6 +1,4 @@ import React, { useState, useEffect, useContext, useRef } from "react"; -import "./Chat.css"; -import PlayerColor from './PlayerColor.js'; import Paper from '@material-ui/core/Paper'; import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; @@ -9,8 +7,10 @@ import Moment from 'react-moment'; import TextField from '@material-ui/core/TextField'; import 'moment-timezone'; -import Resource from './Resource.js'; -import Dice from './Dice.js'; +import "./Chat.css"; +import { PlayerColor } from './PlayerColor.js'; +import { Resource } from './Resource.js'; +import { Dice } from './Dice.js'; import { GlobalContext } from "./GlobalContext.js"; const Chat = () => { diff --git a/client/src/Dice.js b/client/src/Dice.js index a9a68e2..7b4304d 100644 --- a/client/src/Dice.js +++ b/client/src/Dice.js @@ -18,6 +18,6 @@ const Dice = ({ pips }) => { ); }; -export default Dice; +export { Dice }; diff --git a/client/src/PlayerColor.js b/client/src/PlayerColor.js index 04ee338..5830bf0 100644 --- a/client/src/PlayerColor.js +++ b/client/src/PlayerColor.js @@ -14,4 +14,4 @@ const PlayerColor = ({ color }) => { ); }; -export default PlayerColor; \ No newline at end of file +export { PlayerColor }; \ No newline at end of file diff --git a/client/src/PlayerList.js b/client/src/PlayerList.js index fa690e6..51664ff 100644 --- a/client/src/PlayerList.js +++ b/client/src/PlayerList.js @@ -1,9 +1,9 @@ import React, { useState, useEffect, useContext, useRef } from "react"; -import "./PlayerList.css"; -import PlayerColor from './PlayerColor.js'; import Paper from '@material-ui/core/Paper'; import List from '@material-ui/core/List'; +import "./PlayerList.css"; +import { PlayerColor } from './PlayerColor.js'; import { MediaControl } from "./MediaControl.js"; import { GlobalContext } from "./GlobalContext.js"; @@ -18,9 +18,12 @@ const PlayerList = () => { const data = JSON.parse(event.data); switch (data.type) { case 'game-update': + console.log(`player-list - game update`, data.update); + if ('unselected' in data.update) { setUneslected(data.update.unselected); } + if ('players' in data.update) { let found = false; for (let key in data.update.players) { @@ -35,6 +38,7 @@ const PlayerList = () => { } setPlayers(data.update.players); } + if ('state' in data.update && data.update.state !== state) { setState(data.update.state); } diff --git a/client/src/Resource.js b/client/src/Resource.js index 31ea238..88924cf 100644 --- a/client/src/Resource.js +++ b/client/src/Resource.js @@ -39,4 +39,4 @@ const Resource = ({ type, select, disabled, available, count, label, onClick }) ); }; -export default Resource; \ No newline at end of file +export { Resource }; \ No newline at end of file diff --git a/client/src/Table.js b/client/src/Table.js index 0cf3bd6..4dbc654 100755 --- a/client/src/Table.js +++ b/client/src/Table.js @@ -67,64 +67,6 @@ const StartButton = ({ table, game }) => { ); }; -const GameOrder = ({table, game}) => { - - const rollClick = (event) => { - table.throwDice(); - } - - if (!game) { - return (<>); - } - - let players = [], hasRolled = true; - for (let color in game.players) { - const item = game.players[color], - name = getPlayerName(game.sessions, color); - if (!name) { - continue; - } - if (!item.orderRoll) { - item.orderRoll = 0; - } - if (color === game.color) { - hasRolled = item.orderRoll !== 0; - } - players.push({ name: name, color: color, ...item }); - } - - players.sort((A, B) => { - if (A.order === B.order) { - if (A.orderRoll === B.orderRoll) { - return A.name.localeCompare(B.name); - } - return B.orderRoll - A.orderRoll; - } - return B.order - A.order; - }); - - players = players.map(item => -
- -
{item.name}
- { item.orderRoll !== 0 && <>rolled . {item.orderStatus} } - { item.orderRoll === 0 && <>has not rolled yet. {item.orderStatus}} -
- ); - - return ( -
- { game && -
Game Order
-
- { players } -
- -
} -
- ); -}; - const SelectPlayer = ({table, game, players}) => { const playerClick = (event) => { table.stealResource(event.currentTarget.getAttribute('data-color')); @@ -160,78 +102,6 @@ const SelectPlayer = ({table, game, players}) => { ); }; -const Action = ({ table, game }) => { - const buildClicked = (event) => { - table.buildClicked(event); - }; - - const discardClick = (event) => { - const nodes = document.querySelectorAll('.Hand .Resource.Selected'), - discarding = { wheat: 0, brick: 0, sheep: 0, stone: 0, wood: 0 }; - for (let i = 0; i < nodes.length; i++) { - discarding[nodes[i].getAttribute("data-type")]++; - nodes[i].classList.remove('Selected'); - } - return table.discard(discarding); - } - - const newTableClick = (event) => { - return table.shuffleTable(); - }; - - const tradeClick = (event) => { - table.startTrading(); - } - - const rollClick = (event) => { - table.throwDice(); - } - - const passClick = (event) => { - return table.passTurn(); - } -/* - const quitClick = (event) => { - table.setSelected(""); - } -*/ - if (!game.id) { - console.log("Why no game id?"); - return (); - } - - const inLobby = game.state === 'lobby', - inGame = game.state === 'normal', - player = game ? game.player : undefined, - hasRolled = (game && game.turn && game.turn.roll) ? true : false, - isTurn = (game && game.turn && game.turn.color === game.color) ? true : false, - robberActions = (game && game.turn && game.turn.robberInAction), - haveResources = player ? player.haveResources : false, - placement = (game.state === 'initial-placement' || game.turn.active === 'road-building'), - placeRoad = placement && game.turn && game.turn.actions && game.turn.actions.indexOf('place-road') !== -1; - - return ( - - { inLobby && <> - - - } - { !inLobby && <> - - - - { game.turn.roll === 7 && player && player.mustDiscard > 0 && - - } - - } - { /* inLobby && - - */ } - - ); -} - const PlayerName = ({table, game}) => { const [name, setName] = useState(game.name ? game.name : ""); diff --git a/client/src/Trade.js b/client/src/Trade.js index a75ca98..7c59242 100644 --- a/client/src/Trade.js +++ b/client/src/Trade.js @@ -350,6 +350,7 @@ const Trade = ({table}) => { /* Order direction is reversed for self */ source = { name: item.name, + color: item.color, gets: trade.gives, gives: trade.gets }; diff --git a/server/routes/games.js b/server/routes/games.js index c842bc3..ce4dbc0 100755 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -21,7 +21,7 @@ require("../db/games").then(function(db) { gameDB = db; }); -function shuffle(array) { +function shuffleArray(array) { var currentIndex = array.length, temporaryValue, randomIndex; // While there remain elements to shuffle... @@ -122,19 +122,19 @@ const processTies = (players) => { let ties = false, order = 0; /* Reverse from high to low */ slots.reverse().forEach((slot) => { - slot.forEach(pips => { - if (pips.length !== 1) { + slot.forEach(dice => { + if (dice.length !== 1) { ties = true; - pips.forEach(player => { + dice.forEach(player => { player.orderRoll = 0; player.order = order; player.orderStatus = `Tied.`; }); } else { - pips[0].order = order; - pips[0].orderStatus = `Placed in ${order+1}.`; + dice[0].order = order; + dice[0].orderStatus = `Placed in ${order+1}.`; } - order += pips.length + order += dice.length }) }); @@ -142,46 +142,10 @@ const processTies = (players) => { } -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 playerNameFromColor = (game, color) => { - for (let id in game.sessions) { - if (game.sessions[id].color === color) { - return game.sessions[id].name; - } - } - return ''; -}; - -const playerFromColor = (game, color) => { - for (let id in game.sessions) { - if (game.sessions[id].color === color) { - return game.sessions[id].player; - } - } - return undefined; -}; - const playerFromName = (game, name) => { - for (let id in game.sessions) { - if (game.sessions[id].name === name) { - return game.sessions[id].player; + for (let color in game.players) { + if (game.players[color].name === name) { + return game.players[color]; } } return undefined; @@ -189,14 +153,11 @@ const playerFromName = (game, name) => { const processGameOrder = (game, player, dice) => { - let message; - player.orderRoll = dice; - let players = []; + const players = []; let doneRolling = true; - for (let key in game.players) { const tmp = game.players[key]; if (tmp.status === 'Not active') { @@ -205,88 +166,81 @@ const processGameOrder = (game, player, dice) => { 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 = 'initial-placement'; - game.direction = 'forward'; - game.turn = { - name: getPlayerName(game, players[0]), - color: getPlayerColor(game, players[0]) - }; - placeSettlement(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!`); - message = `It is ${game.turn.name}'s turn to place a settlement.`; - } else { - message = `There are still ties for player order!`; - } + /* If 'doneRolling' is FALSE then there are still players to roll */ + if (!doneRolling || !processTies(players)) { + sendUpdateToPlayers(game, { + players: game.players, + chat: game.chat + }) + return; } + + addChatMessage(game, null, `Player order set to ${players + .map((player, index) => { + return `${index+1}. ${player.name}`; + }).join(', ')}.`); - if (message) { - addChatMessage(game, null, message); - } + game.playerOrder = players.map(player => player.color); + game.state = 'initial-placement'; + game.direction = 'forward'; + game.turn = { + name: players[0].name, + color: players[0].color + }; + placeSettlement(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: game.players, + state: game.state, + direction: game.direction, + turn: game.turn, + chat: game.chat, + activities: game.activities + }); } 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!`; + addChatMessage(game, session, `${name} rolled ${Math.ceil(Math.random() * 6)}.`); + sendUpdateToPlayers(game, { chat: game.chat }); + return; case "game-order": if (!player) { - error = `This player is not active!`; - break; + return `This player is not active!`; } - if (player.order && player.orderRoll) { - error = `Player ${name} has already rolled for player order.`; - break; + return `Player ${name} has already rolled for player order.`; } - - 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; + const dice = Math.ceil(Math.random() * 6); + addChatMessage(game, session, `${name} rolled ${dice}.`); + processGameOrder(game, player, dice); + return; case "normal": if (game.turn.color !== session.color) { - error = `It is not your turn.`; - break; + return `It is not your turn.`; } if (game.turn.roll) { - error = `You already rolled this turn.`; - break; + return `You already rolled this turn.`; } - processRoll(game, [ Math.ceil(Math.random() * 6), Math.ceil(Math.random() * 6) ]); - break; + processRoll(game, session, [ Math.ceil(Math.random() * 6), Math.ceil(Math.random() * 6) ]); + return; default: - error = `Invalid game state (${game.state}) in roll.`; - break; + return `Invalid game state (${game.state}) in roll.`; } - - if (!error && message) { - addChatMessage(game, session, message); - } - return error; -}; +} const sessionFromColor = (game, color) => { for (let key in game.sessions) { @@ -296,7 +250,7 @@ const sessionFromColor = (game, color) => { } } -const distributeResources = (session, game, roll) => { +const distributeResources = (game, roll) => { console.log(`Roll: ${roll}`); /* Find which tiles have this roll */ let tiles = []; @@ -344,15 +298,15 @@ const distributeResources = (session, game, roll) => { if (!entry.wood && !entry.brick && !entry.sheep && !entry.wheat && !entry.stone) { continue; } - let message = []; + let message = [], session; for (let type in entry) { if (entry[type] === 0) { continue; } if (color !== 'robber') { - const player = playerFromColor(game, color); - player[type] += entry[type]; + session = sessionFromColor(game, color); + session.player[type] += entry[type]; message.push(`${entry[type]} ${type}`); } else { robberSteal(game, color, type); @@ -360,8 +314,8 @@ const distributeResources = (session, game, roll) => { } } - if (color !== 'robber') { - addChatMessage(game, sessionFromColor(game, color), `${playerNameFromColor(game, color)} receives ${message.join(', ')}.`); + if (session) { + addChatMessage(game, session, `${session.name} receives ${message.join(', ')}.`); } } @@ -385,61 +339,63 @@ const pickRobber = (game) => { } } -const processRoll = (game, dice) => { - let session; - for (let id in game.sessions) { - if (game.sessions[id].name === game.turn.name) { - session = game.sessions[id]; - } +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); + sendUpdateToPlayers(game, { + turn: game.turn, + players: game.players, + chat: game.chat + }); } - if (!session) { - console.error(`Cannot process roll without an active player session`); - return; - } - game.dice = dice; - addChatMessage(game, session, `${session.name} rolled ${game.dice[0]}, ${game.dice[1]}.`); - game.turn.roll = game.dice[0] + game.dice[1]; - if (game.turn.roll === 7) { - 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; - } + /* 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); + } + + 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; } - } else { - mustDiscard.forEach(player => - addChatMessage(game, null, `The robber was rolled and ${getPlayerName(game, player)} must discard ${player.mustDiscard} resource cards!`) - ); + game.turn.limits.pips.push(i); } } else { - distributeResources(session, game, game.turn.roll); + mustDiscard.forEach(player => + addChatMessage(game, null, `The robber was rolled and ${player.name} must discard ${player.mustDiscard} resource cards!`) + ); } + + sendUpdateToPlayers(game, { + turn: game.turn, + players: game.players, + chat: game.chat + }); } -const newPlayer = () => { +const newPlayer = (color) => { return { roads: MAX_ROADS, cities: MAX_CITIES, @@ -454,25 +410,18 @@ const newPlayer = () => { wood: 0, brick: 0, army: 0, - development: [] + development: [], + color }; } -const getPlayer = (game, color) => { - if (!game) { - return newPlayer(); - } - - return game.players[color]; -}; - const getSession = (game, reqSession) => { if (!game.sessions) { game.sessions = {}; } if (!reqSession.player_id) { - reqSession.player_id = crypto.randomBytes(16).toString('hex'); + throw Error(`No session id for ${game.id}`); } const id = reqSession.player_id; @@ -480,6 +429,7 @@ const getSession = (game, reqSession) => { /* 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, 10)}]`, name: undefined, color: undefined, player: undefined, @@ -497,10 +447,10 @@ const getSession = (game, reqSession) => { if (_id === id) { continue; } - /* 10 minutes */ + /* 60 minutes */ const age = Date.now() - _session.lastActive; - if (age > 10 * 60 * 1000) { - console.log(`Expiring old session ${_id}: ${age/(60 * 1000)} minutes`); + if (age > 60 * 60 * 1000) { + console.log(`Expiring old session ${_id}: ${age/(60 * 60 * 1000)} minutes`); delete game.sessions[_id]; if (_id in game.sessions) { console.log('delete DID NOT WORK!'); @@ -527,8 +477,6 @@ const loadGame = async (id) => { return games[id]; } - console.log(`Loading game from disk`); - let game = await readFile(`games/${id}`) .catch(() => { return; @@ -587,7 +535,7 @@ const loadGame = async (id) => { /* Populate the 'unselected' list from the session table */ if (!game.sessions[id].color && game.sessions[id].name) { - game.unselected.push(game.sessions[id]); + game.unselected.push(game.sessions[id].name); } } @@ -596,10 +544,11 @@ const loadGame = async (id) => { }; const clearPlayer = (player) => { + const { color } = player; for (let key in player) { delete player[key]; } - Object.assign(player, newPlayer()); + Object.assign(player, newPlayer(color)); } const canGiveBuilding = (game) => { @@ -755,14 +704,13 @@ const adminActions = (game, action, value) => { addChatMessage(game, null, `Admin rolling ${dice.join(', ')} for ${game.turn.name}.`); switch (game.state) { case 'game-order': - game.dice = dice; - message = `${game.turn.name} rolled ${game.dice[0]}.`; + message = `${game.turn.name} rolled ${dice[0]}.`; addActivity(game, session, message); message = undefined; - processGameOrder(game, session.player, game.dice[0]); + processGameOrder(game, session.player, dice[0]); break; case 'normal': - processRoll(game, dice); + processRoll(game, session, dice); break; } break; @@ -800,8 +748,8 @@ const adminActions = (game, action, value) => { 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) { - session.player = undefined; clearPlayer(player); + session.player = undefined; } session.color = undefined; return; @@ -814,12 +762,12 @@ const adminActions = (game, action, value) => { }; 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.`; } - const id = game.id; - - let rejoin = false; if (!name) { return `You can not set your name to nothing!`; @@ -830,8 +778,9 @@ const setPlayerName = (game, session, name) => { } /* Check to ensure name is not already in use */ - for (let key in game.sessions) { - const tmp = game.sessions[key]; + let rejoin = false; + for (let id in game.sessions) { + const tmp = game.sessions[id]; if (tmp === session || !tmp.name) { continue; } @@ -842,8 +791,7 @@ const setPlayerName = (game, session, name) => { * from active session */ Object.assign(session, tmp, { ws: session.ws }); console.log(`${name} has been reallocated to a new session.`); -// console.log({ old: game.sessions[key], new: session }); - delete game.sessions[key]; + delete game.sessions[id]; } else { return `${name} is already taken and has been active in the last minute.`; } @@ -854,7 +802,6 @@ const setPlayerName = (game, session, name) => { if (!session.name) { message = `A new player has entered the lobby as ${name}.`; - session.name = name; } else { if (rejoin) { if (session.color) { @@ -863,43 +810,61 @@ const setPlayerName = (game, session, name) => { message = `${name} has rejoined the lobby.`; } session.name = name; - if (session.ws && (id in audio) && session.name in audio[id]) { + if (session.ws && (game.id in audio) + && session.name in audio[game.id]) { hasAudio = true; - part(audio[id], session, game.id); + part(audio[game.id], session, game.id); } } else { message = `${session.name} has changed their name to ${name}.`; - if (session.ws && id in audio) { - part(audio[id], session, game.id); + if (session.ws && game.id in audio) { + part(audio[game.id], session, game.id); } - session.name = name; } } - if (!session.color) { - console.log(`Adding ${session.name} to the unselected`); + 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[id], session, game.id); + join(audio[game.id], session, game.id); } console.log(message); addChatMessage(game, null, message); - session.live = true; - if (session.player) { - session.player.live = true; + /* Rebuild the unselected list */ + if (!session.color) { + console.log(`Adding ${session.name} to the unselected`); } - - /* Rebuild the unselected list */ game.unselected = []; for (let id in game.sessions) { if (!game.sessions[id].color && game.sessions[id].name) { - game.unselected.push(game.sessions[id]); + game.unselected.push(game.sessions[id].name); } } - return undefined; + sendUpdateToPlayers(game, { + players: game.players, + unselected: game.unselected, + chat: game.chat + }); + session.ws.send(JSON.stringify({ + type: 'game-update', + update: { + name: session.name, + color: undefined, + live: session.live, + player: session.player + } + })); } const colorToWord = (color) => { @@ -913,87 +878,127 @@ const colorToWord = (color) => { } } -const setPlayerColor = (game, session, color) => { - if (!game) { - return `No game found`; +const getActiveCount = (game) => { + let active = 0; + for (let color in game.players) { + if (game.players[color].name) { + continue; + } + active++; } - - const name = session.name, player = session.player; + 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 (!name) { + 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.`; } - const priorActive = getActiveCount(game); - let message; - - if (player) { + /* 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(player); - if (game.state !== 'lobby') { - message = `${name} has exited to the lobby and is no longer playing as ${colorToWord(session.color)}.` - addChatMessage(game, null, message); - } else { - message = `${name} is no longer ${colorToWord(session.color)}.`; - } + clearPlayer(session.player); session.player = undefined; session.color = undefined; - } - - /* If the player is not selecting a color, then return */ - if (!color) { - if (message) { - console.log(message); - addChatMessage(game, null, message); - } - return; - } - - /* 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 ${colorToWord(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.name); + game.active = active; + if (active === 1) { + addChatMessage(game, null, + `There are no longer enough players to start a game.`); + } + sendUpdateToPlayers(game, { + active: game.active, + unselected: game.unselected, + players: game.players, + chat: game.chat + }); + session.ws.send(JSON.stringify({ + type: 'game-update', + update: { + name: session.name, + color: undefined, + live: session.live, + player: session.player + } + })); + return; } } /* All good -- set this player to requested selection */ - session.player = getPlayer(game, color); - session.player.name = name; - session.player.status = `Active`; - session.player.lastActive = Date.now(); + 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; - game.players[color].name = session.name; - - /* Rebuild the unselected list */ - game.unselected = []; - for (let id in game.sessions) { - if (!game.sessions[id].color && game.sessions[id].name) { - game.unselected.push(game.sessions[id]); - } - } addChatMessage(game, session, `${session.name} has chosen to play as ${colorToWord(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 update = { + players: game.players, + 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].name); } } + if (unselected.length !== game.unselected.length) { + game.unselected = unselected; + update.unselected = game.unselected; + } + + 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; + } + + sendUpdateToPlayers(game, update); + session.ws.send(JSON.stringify({ + type: 'game-update', + update: { + name: session.name, + color: session.color, + live: session.live, + player: session.player + } + })); }; const addActivity = (game, session, message) => { @@ -1014,12 +1019,17 @@ const addChatMessage = (game, session, message) => { now++; } - game.chat.push({ - from: session ? session.name : undefined, - color: session ? session.color : undefined, + 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) => { @@ -1279,21 +1289,23 @@ const calculateRoadLengths = (game, session) => { console.log(currentLongest, currentLength); if (currentLongest && game.players[currentLongest].longestRoad < currentLength) { - addChatMessage(game, session, `${playerNameFromColor(game, currentLongest)} had their longest road split!`); + 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) { - if (game.players[key].status === 'Not active') { + const player = game.players[key]; + if (player.status === 'Not active') { continue; } - if (game.players[key].longestRoad > longestRoad) { + if (player.longestRoad > longestRoad) { longestPlayers = [ key ]; - longestRoad = game.players[key].longestRoad; + longestRoad = player.longestRoad; } else if (game.players[key].longestRoad === longestRoad) { if (longestRoad >= 5) { - longestPlayers.push(key); + longestPlayers.push(player); } } } @@ -1303,16 +1315,16 @@ const calculateRoadLengths = (game, session) => { if (longestPlayers.length > 0) { if (longestPlayers.length === 1) { game.longestRoadLength = longestRoad; - if (game.longestRoad !== longestPlayers[0]) { - game.longestRoad = longestPlayers[0]; + if (game.longestRoad !== longestPlayers[0].color) { + game.longestRoad = longestPlayers[0].color; addChatMessage(game, session, - `${playerNameFromColor(game, game.longestRoad)} now has the longest ` + + `${player.name} now has the longest ` + `road (${longestRoad})!`); } } else { if (checkForTies) { game.longestRoadLength = longestRoad; - const names = longestPlayers.map(color => playerNameFromColor(game, color)); + const names = longestPlayers.map(player => player.name); addChatMessage(game, session, `${names.join(', ')} are tied for longest ` + `road (${longestRoad})!`); } @@ -1495,7 +1507,7 @@ const isSameOffer = (player, offer) => { /* Verifies player can meet the offer */ const checkPlayerOffer = (game, player, offer) => { let error = undefined; - const name = getPlayerName(game, player); + const name = player.name; console.log({ checkPlayerOffer: { name: name, @@ -1629,7 +1641,7 @@ router.put("/:id/:action/:value?", async (req, res) => { return res.status(404).send(error); } - let error; + let error = 'Invalid request'; if ('private-token' in req.headers) { if (req.headers['private-token'] !== req.app.get('admin')) { @@ -1637,538 +1649,462 @@ router.put("/:id/:action/:value?", async (req, res) => { } else { error = adminActions(game, action, value); } - return sendGame(req, res, game, error); - } - - const session = getSession(game, req.session), player = session.player; - - 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, `${session.name}: ${chat.message}`); - /* Chat messages can set game flags and fields */ - const parts = chat.message.match(/^set +([^ ]*) +(.*)$/i); - if (parts && parts.length === 3) { - 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; - } - } - 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, index; - - let corners, corner, card, cards; - - switch (action) { - case "trade": - if (game.state !== "normal") { - error = `Game not in correct state to begin trading.`; - break; - } - - if (!game.turn.actions || game.turn.actions.indexOf('trade') === -1) { - /* Only the active player can begin trading */ - if (game.turn.name !== name) { - error = `You cannot start trading negotiations when it is not your turn.` - break; - } - 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.`); - break; - } - - /* Only the active player can cancel trading */ - if (value === 'cancel') { - /* TODO: Perhaps 'cancel' is how a player can remove an offer... */ - if (game.turn.name !== name) { - error = `Only the active player can cancel trading negotiations.`; - break; - } - game.turn.actions = []; - game.turn.limits = {}; - addActivity(game, session, `${name} has cancelled trading negotiations.`); - break; - } - - /* Any player can make an offer */ - if (value === 'offer') { - const offer = req.body; - - error = checkPlayerOffer(game, session.player, offer); - if (error) { - break; - } - - if (isSameOffer(session.player, offer)) { - console.log(session.player); - error = `You already have a pending offer submitted for ${offerToString(offer)}.`; - break; - } - - 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)}.`); - break; - } - - /* Any player can reject an offer */ - if (value === 'reject') { - const offer = req.body; - - /* If the active player rejected an offer, they rejected another player */ - const other = playerFromName(game, offer.name); - if (!other.offerRejected) { - other.offerRejected = {}; - } - other.offerRejected[session.color] = true; - addActivity(game, session, `${session.name} rejected ${offer.name}'s offer.`); - - break; - } - - /* Only the active player can accept an offer */ - if (value === 'accept') { - if (game.turn.name !== name) { - error = `Only the active player can accept an offer.`; - break; - } - - const offer = req.body; - let target; - - console.log({ offer, description: offerToString(offer) }); - - error = checkPlayerOffer(game, session.player, offer); - if (error) { - break; - } - - /* 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') { - let mismatch = false; - target = game.players[offer.color]; - if (offer.color in target.offerRejected) { - error = `${getPlayerName(game, target)} rejected this offer.`; - break; - } - if (!isCompatibleOffer(target, offer)) { - error = `Unfortunately, trades were re-negotiated in transit and the deal is invalid!`; - break; - } - - error = checkPlayerOffer(game, target, { gives: offer.gets, gets: offer.gives }); - if (error) { - break; - } - - if (!isSameOffer(target, { gives: offer.gets, gets: offer.gives })) { - console.log( { target, offer }); - error = `These terms were not agreed to by ${getPlayerName(game, target)}!`; - break; - } - - if (!canMeetOffer(target, player)) { - error = `${playerNameFromColor(game, offer.color)} cannot meet the terms.`; - break; - } - } else { - target = offer; - } - - debugChat(game, 'Before trade'); - - /* Transfer goods */ - offer.gets.forEach(item => { - if (target.name !== 'The bank') { - target[item.type] -= item.count; - } - player[item.type] += item.count; - }); - offer.gives.forEach(item => { - if (target.name !== 'The bank') { - target[item.type] += item.count; - } - player[item.type] -= 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 = []; - - break; - } - - break; - - 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. New board signature: ${game.signature}.`; - addChatMessage(game, null, message); - console.log(message); + sendGameToPlayers(game); } - break; + } - case 'pass': + return res.status(400).send(error); +}); + +const trade = (game, session, { offer, value }) => { + const name = session.name; + + 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) { - error = `You cannot pass when it isn't your turn.` - break; + return `You cannot start trading negotiations when it is not 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) { - error = `Robber is in action. Turn can not stop until all Robber tasks are resolved.`; - break; + 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; + } - const next = getNextPlayer(game, name); - game.turn = { - name: next, - color: getColorFromName(game, next) - }; - game.turns++; - addActivity(game, session, `${name} passed their turn.`); - addChatMessage(game, null, `It is ${next}'s turn.`); - break; - - case 'place-robber': - if (game.state !== 'normal' && game.turn.roll !== 7) { - error = `You cannot place robber unless 7 was rolled!`; - break; - } + /* Only the active player can cancel trading */ + if (value === 'cancel') { + /* TODO: Perhaps 'cancel' is how a player can remove an offer... */ if (game.turn.name !== name) { - error = `You cannot place the robber when it isn't your turn.`; - break; + return `Only the active player can cancel trading negotiations.`; } - for (let color in game.players) { - if (game.players[color].status === 'Not active') { - continue; - } - if (game.players[color].mustDiscard > 0) { - error = `You cannot place the robber until everyone has discarded!`; - break; - } - } - const robber = parseInt(value ? value : 0); - if (game.robber === robber) { - error = `You must move the robber to a new location!`; - break; - } - game.robber = robber; - game.turn.placedRobber = true; + game.turn.actions = []; + game.turn.limits = {}; + addActivity(game, session, `${name} has cancelled trading negotiations.`); + return; + } - pickRobber(game); - addActivity(game, null, `${game.robberName} Robber Robinson entered the scene as the nefarious robber!`); - - let colors = []; - layout.tiles[robber].corners.forEach(cornerIndex => { - const active = game.placements.corners[cornerIndex]; - if (active && active.color && active.color !== game.turn.color && colors.indexOf(active.color) == -1) { - colors.push(active.color); - } - }); - - if (colors.length) { - game.turn.actions = [ 'steal-resource' ], - game.turn.limits = { players: colors }; - } 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.`); - } - - break; - - case 'steal-resource': - if (game.turn.actions.indexOf('steal-resource') === -1) { - error = `You can only steal a resource when it is valid to do so!`; - break; - } - if (game.turn.limits.players.indexOf(value) === -1) { - error = `You can only steal a resource from a player on this terrain!`; - break; - } - let victim = game.players[value]; - cards = []; - [ 'wheat', 'brick', 'sheep', 'stone', 'wood' ].forEach(field => { - for (let i = 0; i < victim[field]; i++) { - cards.push(field); - } - }); - - debugChat(game, 'Before steal'); - - if (cards.length === 0) { - addChatMessage(game, session, - `${playerNameFromColor(game, value)} ` + - `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[type]--; - session.player[type]++ - game.turn.actions = []; - game.turn.limits = {}; - addChatMessage(game, session, - `${session.name} randomly stole 1 ${type} from ` + - `${playerNameFromColor(game, value)}.`); - } - debugChat(game, 'After steal'); - - game.turn.robberInAction = false; - break; - - case 'buy-development': - if (game.state !== 'normal') { - error = `You cannot purchase a development card unless the game is active.`; - break; - } - if (session.color !== game.turn.color) { - error = `It is not your turn! It is ${game.turn.name}'s turn.`; - break; - } - if (!game.turn.roll) { - error = `You cannot build until you have rolled.`; - break; - } - - if (game.turn && game.turn.robberInAction) { - error = `Robber is in action. You can not purchase until all Robber tasks are resolved.`; - break; - } - - if (player.stone < 1 || player.wheat < 1 || player.sheep < 1) { - error = `You have insufficient resources to purchase a development card.`; - break; - } - if (game.developmentCards.length < 1) { - error = `There are no more development cards!`; - break; - } - if (game.turn.developmentPurchased) { - error = `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--; - debugChat(game, 'After development purchase'); - card = game.developmentCards.pop(); - card.turn = game.turns; - player.development.push(card); - break; - - case 'play-card': - if (game.state !== 'normal') { - error = `You cannot play a development card unless the game is active.`; - break; - } - if (session.color !== game.turn.color) { - error = `It is not your turn! It is ${game.turn.name}'s turn.`; - break; - } - if (!game.turn.roll) { - error = `You cannot play a card until you have rolled.`; - break; + /* Any player can make an offer */ + if (value === 'offer') { + error = checkPlayerOffer(game, session.player, offer); + if (error) { + return error; } - if (game.turn && game.turn.robberInAction) { - error = `Robber is in action. You can not play a card until all Robber tasks are resolved.`; - break; + 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; } - card = req.body; - card = player.development.find(item => item.type == card.type && item.card == card.card); - if (!card) { - error = `The card you want to play was not found in your hand!`; - break; - } - - if (player.playedCard === game.turns && card.type !== 'vp') { - error = `You can only play one development card per turn!`; - break; - } - - if (card.played) { - error = `You have already played this card.`; - break; - } - - /* 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) { - error = `You can not play victory point cards until you can reach 10!`; - break; + /* 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; } - addChatMessage(game, session, `${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.`); - placeRoad(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; + const other = game.players[color]; + if (other.status !== 'Active') { + continue; } - } - 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); + /* 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); } } - break; + addActivity(game, session, `${session.name} submitted an offer to give ${offerToString(offer)}.`); + return; + } + /* Any player can reject an offer */ + if (value === '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 (value === 'accept') { + if (game.turn.name !== name) { + return `Only the active player can accept an offer.`; + } + + const offer = req.body; + let target; + + console.log({ offer, description: offerToString(offer) }); + + error = checkPlayerOffer(game, session.player, offer); + if (error) { + return error; + } + + /* 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!`; + } + + error = checkPlayerOffer(game, target, { gives: offer.gets, gets: offer.gives }); + if (error) { + return error; + } + + 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; + } + player[item.type] += item.count; + }); + offer.gives.forEach(item => { + if (target.name !== 'The bank') { + target[item.type] += item.count; + } + player[item.type] -= 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 shuffle = (game, session) => { + const name = session.name; + + 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); + const message = `${name} requested a new board. New board signature: ${game.signature}.`; + addChatMessage(game, null, message); + console.log(message); +} + +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 = getNextPlayer(game, name); + game.turn = { + name: next, + color: getColorFromName(game, next) + }; + game.turns++; + addActivity(game, session, `${name} passed their turn.`); + addChatMessage(game, null, `It is ${next}'s turn.`); +} + +const placeRobber = (game, session, value) => { + const name = session.name; + + 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!`; + } + } + const robber = parseInt(value ? value : 0); + 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 colors = []; + layout.tiles[robber].corners.forEach(cornerIndex => { + const active = game.placements.corners[cornerIndex]; + if (active && active.color && active.color !== game.turn.color && colors.indexOf(active.color) == -1) { + colors.push(active.color); + } + }); + + if (colors.length) { + game.turn.actions = [ 'steal-resource' ], + game.turn.limits = { players: colors }; + } 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.`); + } +} + +const stealResource = (game, session, value) => { + const name = session.name; + 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.indexOf(value) === -1) { + return `You can only steal a resource from a player on this terrain!`; + } + let victim = game.players[value]; + cards = []; + [ 'wheat', 'brick', 'sheep', 'stone', 'wood' ].forEach(field => { + for (let i = 0; i < victim[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[type]--; + session.player[type]++ + game.turn.actions = []; + game.turn.limits = {}; + addChatMessage(game, session, + `${session.name} randomly stole 1 ${type} from ` + + `${victim.name}.`); + } + debugChat(game, 'After steal'); + + game.turn.robberInAction = false; +} + +const buyDevelopment = (game, session, value) => { + 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--; + debugChat(game, 'After development purchase'); + card = game.developmentCards.pop(); + card.turn = game.turns; + player.development.push(card); +} + +const playCard = (game, session, { card }) => { + const name = session.name; + + 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.`); + placeRoad(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); + } + } +} + +const asdf = () => { + const game = 0, session = 0; + switch (game) { case 'select-resources': if (!game || !game.turn || !game.turn.actions || game.turn.actions.indexOf('select-resources') === -1) { @@ -2239,7 +2175,7 @@ router.put("/:id/:action/:value?", async (req, res) => { continue; } if (player[type]) { - gave.push(`${playerNameFromColor(game, color)} gave ${player[type]} ${type}`); + gave.push(`${player.name} gave ${player[type]} ${type}`); session.player[type] += player[type]; total += player[type]; player[type] = 0; @@ -2323,7 +2259,7 @@ router.put("/:id/:action/:value?", async (req, res) => { } corner = game.placements.corners[index]; if (corner.color) { - error = `This location already has a settlement belonging to ${playerNameFromColor(game, corner.color)}!`; + error = `This location already has a settlement belonging to ${game.players[corner.color].name}!`; break; } @@ -2481,7 +2417,7 @@ router.put("/:id/:action/:value?", async (req, res) => { } corner = game.placements.corners[index]; if (corner.color !== session.color) { - error = `This location already has a settlement belonging to ${playerNameFromColor(game, corner.color)}!`; + error = `This location already has a settlement belonging to ${game.players[corner.color].name}!`; break; } if (corner.type !== 'settlement') { @@ -2575,7 +2511,7 @@ router.put("/:id/:action/:value?", async (req, res) => { } const road = game.placements.roads[index]; if (road.color) { - error = `This location already has a road belonging to ${playerNameFromColor(game, road.color)}!`; + error = `This location already has a road belonging to ${game.players[road.color].name}!`; break; } @@ -2748,13 +2684,11 @@ router.put("/:id/:action/:value?", async (req, res) => { } - - return sendGame(req, res, game, error); -}) +}; const ping = (session) => { if (!session.ws) { - console.log(`Not sending ping to ${session.name} -- connection does not exist.`); + console.log(`[no socket] Not sending ping to ${session.name} -- connection does not exist.`); return; } @@ -2798,12 +2732,18 @@ const setGameState = (game, session, state) => { switch (state) { case "game-order": if (game.state !== 'lobby') { - return `You cannot start a game from other than the lobby.`; + return `You can only start the game from the lobby.`; } 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) => { @@ -2817,14 +2757,14 @@ const join = (peers, session, id) => { const ws = session.ws; if (!session.name) { - console.error(`${id}:${session.id} - join - No name set yet. Audio not available.`); + console.error(`${session.id} - join - No name set yet. Audio not available.`); return; } - console.log(`${id} - join - ${session.name}`); + console.log(`${session.id} - join - ${session.name}`); if (session.name in peers) { - console.log(`${id}:${session.name} - Already joined to Audio.`); + console.log(`${session.id}:${session.name} - Already joined to Audio.`); return; } @@ -2851,10 +2791,10 @@ const getName = (session) => { const part = (peers, session, id) => { const ws = session.ws; - console.log(`${id}:${getName(session)} - Audio part.`); + console.log(`${session.id}:${getName(session)} - Audio part.`); if (!(session.name in peers)) { - console.log(`${id}:${getName(session)} - Does not exist in game audio.`); + console.log(`${session.id}:${getName(session)} - Does not exist in game audio.`); return; } @@ -2902,12 +2842,12 @@ const saveGame = async (game) => { /* Save per turn while debugging... */ await writeFile(`games/${game.id}.${game.turns}`, JSON.stringify(reducedGame, null, 2)) .catch((error) => { - console.error(`Unable to write to games/${game.id}`); + 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(`Unable to write to games/${game.id}`); + console.error(`${session.id} Unable to write to games/${game.id}`); console.error(error); }); } @@ -2930,10 +2870,12 @@ const departLobby = (game, session, color) => { update.chat = game.chat; } - sendToPlayers(game, update); + sendUpdateToPlayers(game, update); } -const sendToPlayers = async (game, update) => { +const sendUpdateToPlayers = async (game, update) => { + const keys = Object.getOwnPropertyNames(update); + console.log(`${game.id} - sendUpdateToPlayers - ${keys.join(',')}`); const message = JSON.stringify({ type: 'game-update', update @@ -2957,17 +2899,68 @@ const getFilteredUnselected = (game) => { .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 sendGameToPlayers = (game) => { + for (let key in game.sessions) { + const _session = game.sessions[key]; + if (!_session.ws) { + continue; + } + _session.ws.send(JSON.stringify({ + type: 'game-update', + update: getFilteredGameForPlayer(game, _session) + })); + } +}; + +const sendError = (session, error) => { + session.ws.send(JSON.stringify({ type: 'error', error })); +} + +const sendWarning = (session, warning) => { + session.ws.send(JSON.stringify({ type: 'warning', warning })); +} + router.ws("/ws/:id", async (ws, req) => { const { id } = req.params; const gameId = id; - ws.id = req.session.player_id; + if (!req.session.player_id) { + req.session.player_id = crypto.randomBytes(16).toString('hex'); + } + const short = `[${req.session.player_id.substring(0, 8)}]`; + ws.id = short; - console.log(`${gameId} - New connection from client.`); + console.log(`${short}:${gameId} - New connection from client.`); if (!(id in audio)) { audio[id] = {}; /* List of peer sockets using session.name as index. */ - console.log(`${id} - New Game Audio`); + console.log(`${short}:${id} - New Game Audio`); } else { - console.log(`${id} - Already has Audio`); + console.log(`${short}:${id} - Already has Audio`); } /* Setup WebSocket event handlers prior to performing any async calls or @@ -3005,12 +2998,12 @@ router.ws("/ws/:id", async (ws, req) => { } session.ws.close(); session.ws = undefined; - console.log(`WebSocket closed for ${getName(session)}`); + console.log(`${short}:WebSocket closed for ${getName(session)}`); } departLobby(game, session); - console.log(`${id}:${ws.id} - closed connection`); + console.log(`${short} - closed connection`); }); ws.on('message', async (message) => { @@ -3018,14 +3011,10 @@ router.ws("/ws/:id", async (ws, req) => { try { data = JSON.parse(message); } catch (error) { - console.error(error, message); + console.error(`${session.id}: parse error`, message); return; } const game = await loadGame(gameId); - if (!game) { - console.error(`Unable to load/create new game for WS request.`); - return; - } const session = getSession(game, req.session); if (!session.ws) { session.ws = ws; @@ -3036,7 +3025,7 @@ router.ws("/ws/:id", async (ws, req) => { session.live = true; session.lastActive = Date.now(); - let error = '', update; + let error, warning, update, processed = true; switch (data.type) { case 'join': @@ -3049,12 +3038,12 @@ router.ws("/ws/:id", async (ws, req) => { case 'relayICECandidate': { if (!(id in audio)) { - console.error(`${id} - relayICECandidate - Does not have Audio`); + console.error(`${session.id}:${id} - relayICECandidate - Does not have Audio`); return; } const { peer_id, ice_candidate } = data.config; - console.log(`${id} - relayICECandidate ${getName(session)} to ${peer_id}`, + console.log(`${short}:${id} - relayICECandidate ${getName(session)} to ${peer_id}`, ice_candidate); message = JSON.stringify({ @@ -3073,7 +3062,7 @@ router.ws("/ws/:id", async (ws, req) => { return; } const { peer_id, session_description } = data.config; - console.log(`${id} - relaySessionDescription ${getName(session)} to ${peer_id}`, + console.log(`${short}:${id} - relaySessionDescription ${getName(session)} to ${peer_id}`, session_description); message = JSON.stringify({ type: 'sessionDescription', @@ -3089,71 +3078,43 @@ router.ws("/ws/:id", async (ws, req) => { break; case 'game-update': - console.log(`Player ${getName(session)} requested a game update.`); + console.log(`${short}:${id}:${getName(session)} - full game update.`); message = JSON.stringify({ type: 'game-update', - update: filterGameForPlayer(game, session) + update: getFilteredGameForPlayer(game, session) }); session.ws.send(message); break; case 'player-name': - console.log(`${id}:${getName(session)} - setPlayerName - ${data.name}`) + console.log(`${short}:${id}:${getName(session)} - setPlayerName - ${data.name}`) error = setPlayerName(game, session, data.name); if (error) { - session.ws.send(JSON.stringify({ type: 'error', error })); + sendError(session, error); break; } - /* Can't use sendToPlayers as the player name is a top level field - * and is unique to each player */ - for (let key in game.sessions) { - const _session = game.sessions[key]; - if (!_session.ws) { - continue; - } - _session.ws.send(JSON.stringify({ - type: 'game-update', - update: filterGameForPlayer(game, _session) - })); - } - console.log('TODO: support only change update. fire update to all players in game'); - await saveGame(game); + saveGame(game); break; case 'set': - console.log(`${id}:${getName(session)} - ${data.type} ${data.field} = ${data.value}`); - update = {}; + console.log(`${short}:${id}:${getName(session)} - ${data.type} ${data.field} = ${data.value}`); switch (data.field) { case 'state': - error = setGameState(game, session, data.value); - if (error) { - console.warn(error); - session.ws.send(JSON.stringify({ type: 'error', error })); - break; + warning = setGameState(game, session, data.value); + if (warning) { + sendWarning(session, warning); + } else { + await saveGame(game); } - sendToPlayers(game, { state: game.state, chat }); break; + case 'color': - error = setPlayerColor(game, session, data.value); - if (error) { - console.warn(error); - session.ws.send(JSON.stringify({ type: 'error', error })); - break; + warning = setPlayerColor(game, session, data.value); + if (warning) { + sendWarning(session, warning); + } else { + await saveGame(game); } - /* Can't use sendToPlayers as the player name is a top level field - * and is unique to each player */ - for (let key in game.sessions) { - const _session = game.sessions[key]; - if (!_session.ws) { - continue; - } - _session.ws.send(JSON.stringify({ - type: 'game-update', - update: filterGameForPlayer(game, _session) - })); - } - console.log('TODO: support only change update. fire update to all players in game'); - await saveGame(game); break; default: console.warn(`WARNING: Requested SET unsupported field: ${data.field}`); @@ -3162,7 +3123,7 @@ router.ws("/ws/:id", async (ws, req) => { break; case 'get': - console.log(`${id}:${getName(session)} - ${data.type} ${data.fields.join(',')}`); + console.log(`${short}:${id}:${getName(session)} - ${data.type} ${data.fields.join(',')}`); update = {}; data.fields.forEach((field) => { switch (field) { @@ -3176,43 +3137,80 @@ router.ws("/ws/:id", async (ws, req) => { update.name = session.name; break; case 'unselected': - update[field] = getFilteredUnselected(game); + update.unselected = getFilteredUnselected(game); break; case 'players': - update[field] = game[field]; - for (let color in game.players) { - if (game.players[color].status !== 'Active') { -// continue; - } - update.players[color] = game.players[color]; - } + update.players = game.players; + break; + case 'color': + update.color = session.color; break; default: if (field in game) { - console.warn(`WARNING: Requested GET not-privatized/sanitized field: ${field}`); - update[field] = game.field; + console.warn(`${short}: WARNING: Requested GET not-privatized/sanitized field: ${field}`); + update[field] = game[field]; } else { if (field in session) { - console.warn(`WARNING: Requested GET not-sanitized field: ${field}`); - update[field] = session.field; + console.warn(`${short}: WARNING: Requested GET not-sanitized session field: ${field}`); + update[field] = session[field]; } else { - console.warn(`WARNING: Requested GET unsupported field: ${field}`); + console.warn(`${short}: WARNING: Requested GET unsupported field: ${field}`); } } break; } }); + console.log(`${short}:${id} - sending update: `, update); message = JSON.stringify({ type: 'game-update', update }); session.ws.send(message); break; - + case 'chat': - console.log(`${id}:${session.id} - ${data.type} - ${data.message}`) + console.log(`${short}:${id} - ${data.type} - ${data.message}`) addChatMessage(game, session, `${session.name}: ${data.message}`); - sendToPlayers(game, { chat: game.chat }); + parseChatCommands(game, data.message); + sendUpdateToPlayers(game, { chat: game.chat }); + break; + + case 'roll': + warning = roll(game, session); + if (warning) { + sendWarning(session, warning); + } + break; + default: + processed = false; + break; + } + + if (processed) { + 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; + } + + switch (data.type) { + case 'shuffle': + error = shuffle(game, session); + if (error) { + sendWarning(session, error); + } else { + sendGameToPlayers(game); + } + break; + + default: + console.warn(`Unsupported request: ${data.type}`); break; } }); @@ -3236,11 +3234,11 @@ router.ws("/ws/:id", async (ws, req) => { } else { addChatMessage(game, null, `${session.name} has rejoined the lobby.`); } - sendToPlayers(game, { chat: game.chat }); + sendUpdateToPlayers(game, { chat: game.chat }); } resetDisconnectCheck(game, req); - console.log(`WebSocket connect from game ${id}:${getName(session)}`); + console.log(`${short}:WebSocket connect from game ${id}:${getName(session)}`); if (session.keepAlive) { clearTimeout(session.keepAlive); @@ -3254,7 +3252,8 @@ const debugChat = (game, preamble) => { let playerInventory = preamble; for (let key in game.players) { - if (game.players[key].status === 'Not active') { + const player = game.players[key]; + if (player.status === 'Not active') { continue; } if (playerInventory !== '') { @@ -3262,9 +3261,9 @@ const debugChat = (game, preamble) => { } else { playerInventory += ' Player' } - playerInventory += ` ${playerNameFromColor(game, key)} has `; + playerInventory += ` ${player.name} has `; const has = [ 'wheat', 'brick', 'sheep', 'stone', 'wood' ].map(resource => { - const count = game.players[key][resource] ? game.players[key][resource] : 0; + const count = player[resource] ? player[resource] : 0; return `${count} ${resource}`; }).filter(item => item !== '').join(', '); if (has) { @@ -3280,15 +3279,6 @@ const debugChat = (game, preamble) => { } } -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 sendGameToSession = (session, reducedSessions, game, reducedGame, error, res) => { const player = session.player ? session.player : undefined; @@ -3333,15 +3323,6 @@ const sendGameToSession = (session, reducedSessions, game, reducedGame, error, r } const sendGame = async (req, res, game, error, wsUpdate) => { - 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." - addChatMessage(game, null, message); - resetGame(game); - } - game.active = active; /* Update the session lastActive clock */ let session; @@ -3389,7 +3370,7 @@ const sendGame = async (req, res, game, error, wsUpdate) => { }); if (!game.winner && (player.points >= 10 && session.color === key)) { - addChatMessage(game, null, `${playerNameFromColor(game, key)} won the game with ${player.points} victory points!`); + addChatMessage(game, null, `${player.name} won the game with ${player.points} victory points!`); game.winner = key; game.state = 'winner'; delete game.turn.roll; @@ -3462,11 +3443,7 @@ const sendGame = async (req, res, game, error, wsUpdate) => { } -const filterGameForPlayer = (game, session) => { - const active = getActiveCount(game); - - game.active = active; - +const getFilteredGameForPlayer = (game, session) => { /* Calculate points and determine if there is a winner */ for (let key in game.players) { const player = game.players[key]; @@ -3587,8 +3564,6 @@ const robberSteal = (game, color, type) => { } const resetGame = (game) => { - console.log(`Reseting ${game.id}`); - Object.assign(game, { startTime: Date.now(), state: 'lobby', @@ -3636,7 +3611,7 @@ const resetGame = (game) => { for (let i = 0; i < layout.roads.length; i++) { game.placements.roads[i] = { color: undefined, - type: undefined + longestRoad: undefined }; } @@ -3660,7 +3635,7 @@ const resetGame = (game) => { card: card })); - shuffle(game.developmentCards); + shuffleArray(game.developmentCards); /* Ensure sessions are connected to player objects */ for (let key in game.sessions) { @@ -3675,7 +3650,6 @@ const resetGame = (game) => { /* Put the robber back on the Desert */ for (let i = 0; i < game.pipOrder.length; i++) { if (game.pipOrder[i] === 18) { - console.log(`Setting robber at ${i}`); game.robber = i; break; } @@ -3700,10 +3674,10 @@ const createGame = (id) => { id: id, developmentCards: [], players: { - O: newPlayer(), - R: newPlayer(), - B: newPlayer(), - W: newPlayer() + O: newPlayer('O'), + R: newPlayer('R'), + B: newPlayer('B'), + W: newPlayer('W') }, sessions: {}, unselected: [] @@ -3726,15 +3700,18 @@ const createGame = (id) => { router.post("/", (req, res/*, next*/) => { console.log("POST games/"); const game = createGame(); + if (!req.session.player_id) { + req.session.player_id = crypto.randomBytes(16).toString('hex'); + } + const session = getSession(game, req.session); saveGame(game); - - return res.status(200).send(filterGameForPlayer(game, session)); + return res.status(200).send(getFilteredGameForPlayer(game, session)); }); const setBeginnerGame = (game) => { pickRobber(game); - shuffle(game.developmentCards); + shuffleArray(game.developmentCards); game.borderOrder = []; for (let i = 0; i < 6; i++) { game.borderOrder.push(i); @@ -3765,13 +3742,13 @@ const shuffleBoard = (game) => { for (let i = 0; i < 6; i++) { seq.push(i); } - shuffle(seq); + shuffleArray(seq); game.borderOrder = seq.slice(); for (let i = 6; i < 19; i++) { seq.push(i); } - shuffle(seq); + shuffleArray(seq); game.tileOrder = seq.slice(); /* Pip order is from one of the random corners, then rotate around @@ -3808,7 +3785,7 @@ const shuffleBoard = (game) => { } } - shuffle(game.developmentCards); + shuffleArray(game.developmentCards); game.signature = gameSignature(game); }