diff --git a/client/src/Activities.js b/client/src/Activities.js index 07c35c8..633e810 100644 --- a/client/src/Activities.js +++ b/client/src/Activities.js @@ -1,8 +1,6 @@ -import React, { useState, useCallback, useEffect } from "react"; +import React, { useState } from "react"; import "./Activities.css"; -import Paper from '@material-ui/core/Paper'; -import Resource from './Resource.js'; -import { getPlayerName } from './Common.js'; + import PlayerColor from './PlayerColor.js'; import Dice from './Dice.js'; @@ -65,33 +63,41 @@ const Activities = ({ table }) => { normalPlay = (game.state === 'initial-placement' || game.state === 'normal'), mustDiscard = game.player ? parseInt(game.player.mustDiscard ? game.player.mustDiscard : 0) : 0, mustPlaceRobber = (game.turn && !game.turn.placedRobber && game.turn.robberInAction), - isInitialPlacement = (game.state == 'initial-placement'), - placeRoad = isInitialPlacement && game.turn && game.turn.actions.indexOf('place-road') !== -1; + isInitialPlacement = (game.state === 'initial-placement'), + placeRoad = isInitialPlacement && game.turn && game.turn.actions && game.turn.actions.indexOf('place-road') !== -1, + mustStealResource = game.turn && game.turn.actions && game.turn.actions.indexOf('steal-resource') !== -1; const list = game.activities .filter(activity => game.timestamp - activity.date < 11000) .map(activity => { return ; }); - + + let who; + if (isTurn) { + who = 'You'; + } else { + who = <> {table.game.turn.name} + } return (
{ list } - { !isTurn && normalPlay && game.player && mustDiscard === 0 && mustPlaceRobber && -
{table.game.turn.name} must move the Robber.
+ { normalPlay && mustDiscard === 0 && mustPlaceRobber && +
{who} must move the Robber.
} - { isTurn && normalPlay && game.player && mustDiscard === 0 && mustPlaceRobber && -
You must move the Robber.
+ { isInitialPlacement && +
{who} must place a {placeRoad ? 'road' : 'settlement'}.
} - { normalPlay && game.player && mustDiscard !== 0 && + { mustStealResource && +
{who} must select a player to steal from.
+ } + + { normalPlay && mustDiscard !== 0 &&
You must discard {mustDiscard} cards.
} - { isTurn && isInitialPlacement && -
You must place a {placeRoad ? 'road' : 'settlement'}.
- } { !isTurn && normalPlay &&
It is {table.game.turn.name}'s turn.
diff --git a/client/src/Table.js b/client/src/Table.js index 1a4d940..5369c17 100755 --- a/client/src/Table.js +++ b/client/src/Table.js @@ -738,7 +738,7 @@ class Table extends React.Component { if (isDead) { console.log(`Short circuiting keep-alive`); } else { - console.log(`Resetting keep-alive`); + console.log(`${this.game.name} Resetting keep-alive: ${(Date.now() - this.game.startTime) / 1000}`); } if (this.keepAlive) { @@ -749,7 +749,7 @@ class Table extends React.Component { } this.keepAlive = setTimeout(() => { - console.error(`No server ping after 10 seconds (or connection closed by server)!`); + console.log(`${this.game.name} No ping after 10 seconds: ${(Date.now() - this.game.startTime) / 1000}`); this.setState({ noNetwork: true }); if (this.ws) { this.ws.close(); @@ -782,12 +782,13 @@ class Table extends React.Component { this.ws = new WebSocket(new_uri); - this.ws.onopen = (event) => { - console.log(`WebSocket open:`, event); + this.ws.addEventListener('open', (event) => { + console.log(`${this.game.name} WebSocket open: Sending game-update request: ${(Date.now() - this.game.startTime) / 1000}`); + this.ws.send(JSON.stringify({ type: 'game-update' })); this.resetKeepAlive(); - }; + }); - this.ws.onmessage = (event) => { + this.ws.addEventListener('message', (event) => { this.resetKeepAlive(); let data; @@ -813,19 +814,19 @@ class Table extends React.Component { console.log(`Unknown event type: ${data.type}`); break; } - } + }); - this.ws.onerror = (event) => { + this.ws.addEventListener('error', (event) => { this.setState({ error: event.message }); - console.error(`WebSocket error:`, event); + console.error(`${this.game.name} WebSocket error: ${(Date.now() - this.game.startTime) / 1000}`); this.resetKeepAlive(true); - }; + }); - this.ws.onclose = (event) => { - console.error(`WebSocket close:`, event); + this.ws.addEventListener('close', (event) => { + console.log(`${this.game.name} WebSocket close: ${(Date.now() - this.game.startTime) / 1000}`); this.setState({ error: event.message }); this.resetKeepAlive(true); - }; + }); } componentDidMount() { diff --git a/server/routes/games.js b/server/routes/games.js index 85e9fd0..b85cf3e 100755 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -445,8 +445,7 @@ const getSession = (game, session) => { const id = session.player_id; - /* If this session is not yet in the game, - * add it and set the player's name */ + /* If this session is not yet in the game, add it and set the player's name */ if (!(id in game.sessions)) { game.sessions[id] = { name: undefined, @@ -455,6 +454,22 @@ const getSession = (game, session) => { }; } + /* Expire old unused sessions */ + for (let id in game.sessions) { + const tmp = game.sessions[id]; + if (tmp.color || tmp.name || tmp.player) { + continue; + } + if (tmp.player_id === session.player_id) { + continue; + } + /* 10 minutes */ + if (tmp.lastActive && tmp.lastActive < Date.now() - 10 * 60 * 1000) { + console.log(`Expiring old session ${id}`); + delete game.sessions[id]; + } + } + return game.sessions[id]; }; @@ -1762,7 +1777,6 @@ router.put("/:id/:action/:value?", async (req, res) => { if (colors.length) { game.turn.actions = [ 'steal-resource' ], game.turn.limits = { players: colors }; - addChatMessage(game, session, `${session.name} must select player to steal resource from.`); } else { game.turn.actions = []; game.turn.robberInAction = false; @@ -1844,6 +1858,7 @@ router.put("/:id/:action/:value?", async (req, res) => { debugChat(game, 'Before development purchase'); addActivity(game, session, `${session.name} purchased a development card.`); + addChatMessage(game, session, `${session.name} spent 1 stone, 1 wheat, and 1 sheep to purchase a development card.`) player.stone--; player.wheat--; player.sheep--; @@ -2105,6 +2120,7 @@ router.put("/:id/:action/:value?", async (req, res) => { player.settlements--; if (!game.turn.free) { + addChatMessage(game, session, `${session.name} spent 1 brick, 1 wood, 1 sheep, and 1 wheat to purchase a settlement.`) player.brick--; player.wood--; player.wheat--; @@ -2263,6 +2279,7 @@ router.put("/:id/:action/:value?", async (req, res) => { player.cities--; player.settlements++; if (!game.turn.free) { + addChatMessage(game, session, `${session.name} spent 2 wheat, and 1 stone to upgrade to a city.`) player.wheat -= 2; player.stone -= 3; } @@ -2351,6 +2368,7 @@ router.put("/:id/:action/:value?", async (req, res) => { player.roads--; if (!game.turn.free) { + addChatMessage(game, session, `${session.name} spent 1 brick and 1 wood to purchase a road.`) player.brick--; player.wood--; } @@ -2515,14 +2533,50 @@ const ping = (session) => { if (session.keepAlive) { clearTimeout(session.keepAlive); } - session.keepAlive = setTimeout(() => { ping(session); }, 2500); + session.keepAlive = setTimeout(() => { ping(session); }, 2500); } router.ws("/ws/:id", async (ws, req) => { const { id } = req.params; - console.log(`WebSocket connect from game ${id}`); + /* Setup WebSocket event handlers prior to performing any async calls or + * we may miss the first messages from clients */ + ws.on('error', (event) => { + console.error(`WebSocket error: `, event.message); + }); + + ws.on('open', (event) => { + console.log(`WebSocket open: `, event.message); + }); + + ws.on('message', async (message) => { + /* Ensure the session is loaded prior to the first 'message' + * being processed */ + const game = await loadGame(id); + if (!game) { + console.error(`Unable to load/create new game for WS request.`); + return; + } + const session = getSession(game, req.session); + try { + const data = JSON.parse(message); + switch (data.type) { + case 'pong': + console.log(`Latency for ${session.name ? session.name : 'Unammed'} is ${Date.now() - data.timestamp}`); + break; + case 'game-update': + console.log(`Player ${session.name ? session.name : 'Unnamed'} requested a game update.`); + sendGame(req, undefined, game, undefined, ws); + break; + } + } catch (error) { + console.error(error); + } + }); + /* This will result in the node tick moving forward; if we haven't already + * setup the event handlers, a 'message' could come through prior to this + * completing */ const game = await loadGame(id); if (!game) { console.error(`Unable to load/create new game for WS request.`); @@ -2530,7 +2584,9 @@ router.ws("/ws/:id", async (ws, req) => { } const session = getSession(game, req.session); - + + console.log(`WebSocket connect from game ${id}:${session.name ? session.name : "Unnamed"}`); + if (session) { console.log(`WebSocket connected for ${session.name ? session.name : "Unnamed"}`); session.ws = ws; @@ -2541,27 +2597,6 @@ router.ws("/ws/:id", async (ws, req) => { } else { console.log(`No session found for WebSocket with id ${id}`); } - - ws.on('error', (event) => { - console.error(`WebSocket error: `, event.message); - }); - - ws.on('open', (event) => { - console.log(`WebSocket open: `, event.message); - }); - - ws.on('message', (message) => { - try { - const data = JSON.parse(message); - switch (data.type) { - case 'pong': - console.log(`Latency for ${session.name ? session.name : 'Unammed'} is ${Date.now() - data.timestamp}`); - break; - } - } catch (error) { - console.error(error); - } - }); }); router.get("/:id", async (req, res/*, next*/) => { @@ -2619,7 +2654,50 @@ const getActiveCount = (game) => { return active; } -const sendGame = async (req, res, game, error) => { +const sendGameToSession = (session, reducedSessions, game, reducedGame, error, res) => { + const player = session.player ? session.player : undefined; + + if (player) { + player.haveResources = player.wheat > 0 || + player.brick > 0 || + player.sheep > 0 || + player.stone > 0 || + player.wood > 0; + } + + /* Strip out data that should not be shared with players */ + delete reducedGame.developmentCards; + + const playerGame = Object.assign({}, reducedGame, { + timestamp: Date.now(), + status: error ? error : "success", + name: session.name, + color: session.color, + order: (session.color in game.players) ? game.players[session.color].order : 0, + player: player, + sessions: reducedSessions, + layout: layout + }); + + if (!res) { + if (!error) { + if (!session.ws) { + console.error(`No WebSocket connection to ${session.name}`); + } else { + console.log(`Sending update to ${session.name}`); + session.ws.send(JSON.stringify({ + type: 'game-update', + update: playerGame + })); + } + } + } else { + console.log(`Returning update to ${session.name ? session.name : 'Unnamed'}`); + res.status(200).send(playerGame); + } +} + +const sendGame = async (req, res, game, error, wsUpdate) => { const active = getActiveCount(game); /* Enforce game limit of >= 2 players */ @@ -2726,60 +2804,33 @@ const sendGame = async (req, res, game, error) => { reducedSessions.push(reduced); } - /* 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(error); - }); - await writeFile(`games/${game.id}`, JSON.stringify(reducedGame, null, 2)) - .catch((error) => { - console.error(`Unable to write to games/${game.id}`); - console.error(error); - }); - - for (let id in game.sessions) { - const target = game.sessions[id], - useWS = target !== session, - player = target.player ? target.player : undefined; - - if (player) { - player.haveResources = player.wheat > 0 || - player.brick > 0 || - player.sheep > 0 || - player.stone > 0 || - player.wood > 0; - } - - /* Strip out data that should not be shared with players */ - delete reducedGame.developmentCards; - - const playerGame = Object.assign({}, reducedGame, { - timestamp: Date.now(), - status: error ? error : "success", - name: target.name, - color: target.color, - order: (target.color in game.players) ? game.players[target.color].order : 0, - player: player, - sessions: reducedSessions, - layout: layout + if (!wsUpdate) { + /* 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(error); }); + await writeFile(`games/${game.id}`, JSON.stringify(reducedGame, null, 2)) + .catch((error) => { + console.error(`Unable to write to games/${game.id}`); + console.error(error); + }); + } - if (useWS) { - if (!error) { - if (!target.ws) { - console.error(`No WebSocket connection to ${target.name}`); - } else { - console.log(`Sending update to ${target.name}`); - target.ws.send(JSON.stringify({ - type: 'game-update', - update: playerGame - })); + if (wsUpdate) { + /* This is a one-shot request from a client to send the game-update over WebSocket */ + sendGameToSession(session, reducedSessions, game, reducedGame); + } else { + for (let id in game.sessions) { + const target = game.sessions[id], useWS = target !== session; + if (useWS) { + if (!error) { + sendGameToSession(target, reducedSessions, game, reducedGame); } + } else { + sendGameToSession(target, reducedSessions, game, reducedGame, error, res); } - } else { - console.log(`Returning update to ${target.name ? target.name : 'Unnamed'}`); - res.status(200).send(playerGame); } } }