- { isSelf &&
- { mic &&
}
- { !mic &&
}
+ if (!control) {
+ return
+ { isSelf && }
+ { !isSelf && }
+
;
+ }
+
+ return
+ { isSelf &&
+ { control.muted && }
+ { !control.muted && }
}
-
- { mute && }
- { !mute && }
-
-
- ;
+ { !isSelf &&
+ { control.muted && }
+ { !control.muted && }
+
}
+
;
};
-export { MediaControl, MediaAgent, MediaContext };
+export { MediaControl, MediaAgent };
diff --git a/client/src/PingPong.css b/client/src/PingPong.css
new file mode 100644
index 0000000..257cc0c
--- /dev/null
+++ b/client/src/PingPong.css
@@ -0,0 +1,7 @@
+.PingPong {
+ position: absolute;
+ display: flex;
+ top: 0;
+ left: 0;
+ z-index: 100;
+}
\ No newline at end of file
diff --git a/client/src/PingPong.js b/client/src/PingPong.js
new file mode 100644
index 0000000..1380a5f
--- /dev/null
+++ b/client/src/PingPong.js
@@ -0,0 +1,41 @@
+
+import React, { useState, useContext, useEffect, useRef } from "react";
+import { GlobalContext} from "./GlobalContext.js";
+import "./PingPong.css";
+
+const PingPong = () => {
+ const [ count, setCount ] = useState(0);
+ const global = useContext(GlobalContext);
+
+ const onWsMessage = (event) => {
+ const data = JSON.parse(event.data);
+ switch (data.type) {
+ case 'ping':
+ global.ws.send(JSON.stringify({ type: 'pong', timestamp: data.ping }));
+ setCount(count + 1);
+ break;
+ default:
+ break;
+ }
+ };
+ const refWsMessage = useRef(onWsMessage);
+
+ useEffect(() => { refWsMessage.current = onWsMessage; });
+
+ useEffect(() => {
+ if (!global.ws) {
+ return;
+ }
+ const cbMessage = e => refWsMessage.current(e);
+ global.ws.addEventListener('message', cbMessage);
+ return () => {
+ global.ws.removeEventListener('message', cbMessage);
+ }
+ }, [global.ws, refWsMessage]);
+
+ return
+ Game {global.gameId}: {global.name} {global.ws ? 'has socket' : 'no socket' } { count } pings
+
;
+}
+
+export { PingPong };
\ No newline at end of file
diff --git a/client/src/PlayerList.css b/client/src/PlayerList.css
new file mode 100644
index 0000000..cb10d2b
--- /dev/null
+++ b/client/src/PlayerList.css
@@ -0,0 +1,81 @@
+.PlayerList {
+ display: flex;
+ position: relative;
+ padding: 0.5em;
+ user-select: none;
+}
+
+.PlayerList .PlayerSelector .PlayerColor {
+ width: 1em;
+ height: 1em;
+}
+
+.PlayerList .PlayerSelector {
+ display: inline-flex;
+ flex-wrap: wrap;
+ flex-direction: row;
+ justify-content: space-between;
+}
+
+.PlayerList .PlayerSelector.MuiList-padding {
+ padding: 0;
+}
+
+.PlayerList .PlayerSelector .MuiTypography-body1 {
+ font-size: 0.8rem;
+/* white-space: nowrap;*/
+}
+
+.PlayerList .PlayerSelector .MuiTypography-body2 {
+ font-size: 0.7rem;
+ white-space: nowrap;
+}
+
+.PlayerList .PlayerEntry {
+ border: 1px solid rgba(0,0,0,0);
+ border-radius: 0.5em;
+ min-width: 10em;
+ justify-content: space-between;
+}
+
+.PlayerList .PlayerSelector .PlayerEntry {
+ flex: 1 1 0px;
+ align-items: center;
+ display: inline-flex;
+ flex-direction: row;
+ min-width: 10em;
+}
+
+.PlayerList .PlayerEntry[data-selectable=true]:hover {
+ border-color: rgba(0,0,0,0.5);
+ cursor: pointer;
+}
+
+.PlayerList .PlayerEntry[data-selected=true] {
+ background-color: rgba(255, 255, 0, 0.5);
+}
+
+.PlayerList .PlayerEntry > *:last-child {
+ display: flex;
+ flex-grow: 1;
+}
+
+
+.PlayerList .Players .PlayerToggle {
+ min-width: 5em;
+ display: inline-flex;
+ align-items: flex-end;
+ flex-direction: column;
+}
+
+.PlayerList .PlayerName {
+ padding: 0.5em;
+}
+
+.PlayerList .Players > * {
+ width: 100%;
+}
+
+.PlayerList .Players .nameInput {
+ flex-grow: 1;
+}
\ No newline at end of file
diff --git a/client/src/PlayerList.js b/client/src/PlayerList.js
new file mode 100644
index 0000000..05c8f22
--- /dev/null
+++ b/client/src/PlayerList.js
@@ -0,0 +1,100 @@
+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 { MediaControl } from "./MediaControl.js";
+import { GlobalContext } from "./GlobalContext.js";
+
+const PlayerList = () => {
+ const { ws, name } = useContext(GlobalContext);
+ const [players, setPlayers] = useState({});
+ const [state, setState] = useState('lobby');
+ const [color, setColor] = useState(undefined);
+
+ const onWsMessage = (event) => {
+ const data = JSON.parse(event.data);
+ switch (data.type) {
+ case 'game-update':
+ if (data.update.players) {
+ for (let key in data.update.players) {
+ if (data.update.players[key].name === name) {
+ setColor(key);
+ break;
+ }
+ }
+ setPlayers(data.update.players);
+ }
+ if (data.update.state) {
+ setState(data.update.state);
+ }
+ break;
+ default:
+ break;
+ }
+ };
+ const refWsMessage = useRef(onWsMessage);
+
+ useEffect(() => { refWsMessage.current = onWsMessage; });
+
+ useEffect(() => {
+ if (!ws) {
+ return;
+ }
+ const cbMessage = e => refWsMessage.current(e);
+ ws.addEventListener('message', cbMessage);
+ return () => {
+ ws.removeEventListener('message', cbMessage);
+ }
+ }, [ws, refWsMessage]);
+
+ useEffect(() => {
+ if (!ws) {
+ return;
+ }
+ ws.send(JSON.stringify({
+ type: 'get',
+ fields: [ 'state', 'players' ]
+ }));
+ }, [ws]);
+
+ const toggleSelected = (key) => {
+ ws.send(JSON.stringify({
+ type: 'set',
+ field: 'color',
+ value: color === key ? "" : key
+ }));
+ }
+
+ const playerElements = [];
+
+ const inLobby = state === 'lobby';
+ for (let key in players) {
+ const item = players[key];
+ const name = item.name;
+ const selectable = inLobby && (item.status === 'Not active' || color === key);
+ playerElements.push(
+
{ inLobby && selectable && toggleSelected(key) }}
+ key={`player-${key}`}>
+
{name ? name : 'Available' }
+ { name && }
+ { !name && }
+
+ );
+ }
+
+ return (
+
+
+ { playerElements }
+
+
+ );
+}
+
+export { PlayerList };
\ No newline at end of file
diff --git a/client/src/PlayerName.css b/client/src/PlayerName.css
new file mode 100644
index 0000000..28b2b1d
--- /dev/null
+++ b/client/src/PlayerName.css
@@ -0,0 +1,17 @@
+.PlayerName {
+ padding: 0.5em;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: row;
+}
+
+.PlayerName > .nameInput {
+ margin-right: 1em;
+ flex: 1;
+ max-width: 30em;
+}
+
+.PlayerName > Button {
+ background: lightblue;
+}
diff --git a/client/src/PlayerName.js b/client/src/PlayerName.js
new file mode 100644
index 0000000..1494a4f
--- /dev/null
+++ b/client/src/PlayerName.js
@@ -0,0 +1,82 @@
+import React, { useState, useEffect, useContext, useRef } from "react";
+import "./PlayerName.css";
+import Paper from '@material-ui/core/Paper';
+import TextField from '@material-ui/core/TextField';
+import Button from '@material-ui/core/Button';
+
+import { GlobalContext } from "./GlobalContext.js";
+
+const PlayerName = () => {
+ const global = useContext(GlobalContext);
+ const [name, setName] = useState(global.name ? global.name : "");
+ const [error, setError] = useState("");
+
+ const onWsMessage = (event) => {
+ const data = JSON.parse(event.data);
+ switch (data.type) {
+ case 'game-update':
+ if ('name' in data.update && data.update.name !== name) {
+ setName(data.update.name);
+ }
+ break;
+
+ case 'player-name':
+ if ('error' in data) {
+ setError(data.error);
+ }
+ break;
+
+ default:
+ break;
+ }
+ };
+ const refWsMessage = useRef(onWsMessage);
+
+ useEffect(() => { refWsMessage.current = onWsMessage; });
+
+ useEffect(() => {
+ if (!global.ws) {
+ return;
+ }
+ const cbMessage = e => refWsMessage.current(e);
+ global.ws.addEventListener('message', cbMessage);
+ return () => {
+ global.ws.removeEventListener('message', cbMessage);
+ }
+ }, [global.ws, refWsMessage]);
+
+ const sendName = () => {
+ if (name !== global.name && name !== "") {
+ if (error) {
+ setError("");
+ }
+ global.ws.send(JSON.stringify({ type: 'player-name', name }));
+ }
+ }
+
+ const nameChange = (event) => {
+ setName(event.target.value);
+ }
+
+ const nameKeyPress = (event) => {
+ if (event.key === "Enter") {
+ sendName();
+ }
+ }
+
+ return (
+
+ { error !== "" && Error: {error}
}
+
+
+
+ );
+};
+
+export { PlayerName };
\ No newline at end of file
diff --git a/client/src/Table.css b/client/src/Table.css
index 53274bc..f4d63b9 100755
--- a/client/src/Table.css
+++ b/client/src/Table.css
@@ -9,6 +9,13 @@
background-image: url("./assets/tabletop.png");
}
+.Table .Chat {
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 30rem;
+}
+
.Loading {
position: absolute;
top: 1em;
@@ -230,96 +237,6 @@
.Game.lobby {
}
-/*
- * Game
- * Message
- * Players
- * Chat
- * Action
- */
-.Players {
- flex: 1 0;
- overflow: hidden;
- padding: 0.5em;
- user-select: none;
-}
-
-.PlayerSelector .PlayerColor {
- width: 1em;
- height: 1em;
-}
-
-.PlayerSelector {
- display: inline-flex;
- flex-wrap: wrap;
- flex-direction: row;
- justify-content: space-between;
-}
-
-.PlayerSelector.MuiList-padding {
- padding: 0;
-}
-
-.PlayerSelector .MuiTypography-body1 {
- font-size: 0.8rem;
-/* white-space: nowrap;*/
-}
-
-.PlayerSelector .MuiTypography-body2 {
- font-size: 0.7rem;
- white-space: nowrap;
-}
-
-.Players .PlayerEntry {
- border: 1px solid rgba(0,0,0,0);
- border-radius: 0.5em;
- min-width: 10em;
- justify-content: space-between;
-}
-
-.PlayerSelector .PlayerEntry {
- flex: 1 1 0px;
- align-items: center;
- display: inline-flex;
- flex-direction: row;
- min-width: 10em;
-}
-
-.Players .PlayerEntry[data-selectable=true]:hover {
- border-color: rgba(0,0,0,0.5);
- cursor: pointer;
-}
-
-.Players .PlayerEntry[data-selected=true] {
- background-color: rgba(255, 255, 0, 0.5);
-}
-
-.Players .PlayerEntry > *:last-child {
- display: flex;
- flex-grow: 1;
-}
-
-
-.Players .PlayerToggle {
- min-width: 5em;
- display: inline-flex;
- align-items: flex-end;
- flex-direction: column;
-}
-
-.PlayerName {
- padding: 0.5em;
-}
-
-.Players > * {
- width: 100%;
-}
-
-.Players .nameInput {
- flex-grow: 1;
-}
-
-
.Development {
position: relative;
display: inline-block;
@@ -380,7 +297,9 @@ button {
display: inline-flex;
}
+
.PlayerName {
+ padding: 0.5em;
display: flex;
align-items: center;
justify-content: center;
diff --git a/server/routes/games.js b/server/routes/games.js
index 68f3012..c3be448 100755
--- a/server/routes/games.js
+++ b/server/routes/games.js
@@ -466,42 +466,52 @@ const getPlayer = (game, color) => {
return game.players[color];
};
-const getSession = (game, session) => {
+const getSession = (game, reqSession) => {
if (!game.sessions) {
game.sessions = {};
}
- if (!session.player_id) {
- session.player_id = crypto.randomBytes(16).toString('hex');
+ if (!reqSession.player_id) {
+ reqSession.player_id = crypto.randomBytes(16).toString('hex');
}
- const id = session.player_id;
+ const id = reqSession.player_id;
/* If this session is not yet in the game, add it and set the player's name */
if (!(id in game.sessions)) {
game.sessions[id] = {
name: undefined,
color: undefined,
- player: undefined
+ player: undefined,
+ lastActive: Date.now()
};
}
/* Expire old unused sessions */
- for (let id in game.sessions) {
- const tmp = game.sessions[id];
- if (tmp.color || tmp.name || tmp.player) {
+ for (let _id in game.sessions) {
+ const _session = game.sessions[_id];
+ if (_session.color || _session.name || _session.player) {
continue;
}
- if (tmp.player_id === session.player_id) {
+ if (_id === id) {
continue;
}
/* 10 minutes */
- if (tmp.lastActive && tmp.lastActive < Date.now() - 10 * 60 * 1000) {
- console.log(`Expiring old session ${id}`);
- delete game.sessions[id];
+ const age = Date.now() - _session.lastActive;
+ if (age > 10 * 60 * 1000) {
+ console.log(`Expiring old session ${_id}: ${age/(60 * 1000)} minutes`);
+ delete game.sessions[_id];
+ if (_id in game.sessions) {
+ console.log('delete DID NOT WORK!');
+ }
}
}
+ session.lastActive = Date.now();
+ if (session.player) {
+ session.player.lastActive = session.lastActive;
+ }
+
return game.sessions[id];
};
@@ -514,6 +524,8 @@ const loadGame = async (id) => {
return games[id];
}
+ console.log(`Loading game from disk`);
+
let game = await readFile(`games/${id}`)
.catch(() => {
return;
@@ -785,7 +797,7 @@ const setPlayerName = (game, session, name) => {
return `You cannot change your name while you have a color selected.`;
}
const id = game.id;
-
+
let rejoin = false;
/* Check to ensure name is not already in use */
if (game && name) for (let key in game.sessions) {
@@ -796,9 +808,11 @@ const setPlayerName = (game, session, name) => {
if (tmp.name && tmp.name.toLowerCase() === name.toLowerCase()) {
if (!tmp.player || (Date.now() - tmp.player.lastActive) > 60000) {
rejoin = true;
- Object.assign(session, tmp);
+ /* Update the session object from tmp, but retain websocket
+ * 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 });
+// console.log({ old: game.sessions[key], new: session });
delete game.sessions[key];
} else {
return `${name} is already taken and has been active in the last minute.`;
@@ -917,6 +931,8 @@ const setPlayerColor = (game, session, color) => {
session.player.status = `Active`;
session.player.lastActive = Date.now();
session.color = color;
+ game.players[color].name = session.name;
+
addChatMessage(game, session, `${session.name} has chosen to play as ${colorToWord(color)}.`);
const afterActive = getActiveCount(game);
@@ -937,10 +953,19 @@ const addActivity = (game, session, message) => {
}
const addChatMessage = (game, session, message) => {
+ let now = Date.now();
+ let lastTime = 0;
+ if (game.chat.length) {
+ lastTime = game.chat[game.chat.length - 1].date;
+ }
+ if (now === lastTime) {
+ now++;
+ }
+
game.chat.push({
from: session ? session.name : undefined,
color: session ? session.color : undefined,
- date: Date.now(),
+ date: now,
message: message
});
};
@@ -2752,7 +2777,7 @@ const join = (peers, session, id) => {
for (let peer in peers) {
peers[peer].send(JSON.stringify({
type: 'addPeer',
- data: { 'peer_id': peer, 'should_create_offer': false }
+ data: { 'peer_id': session.name, 'should_create_offer': false }
}));
ws.send(JSON.stringify({
@@ -2765,13 +2790,17 @@ const join = (peers, session, id) => {
peers[session.name] = ws;
};
+const getName = (session) => {
+ return session.name ? session.name : "Unnamed";
+}
+
const part = (peers, session, id) => {
const ws = session.ws;
- console.log(`${id}:${session.name} - Audio part.`);
+ console.log(`${id}:${getName(session)} - Audio part.`);
if (!(session.name in peers)) {
- console.log(`${id}:${session.name} - Does not exist in game audio.`);
+ console.log(`${id}:${getName(session)} - Does not exist in game audio.`);
return;
}
@@ -2791,11 +2820,48 @@ const part = (peers, session, id) => {
}
};
+const saveGame = async (game) => {
+ /* Shallow copy game, filling its sessions with a shallow copy of sessions so we can then
+ * delete the player field from them */
+ const reducedGame = Object.assign({}, game, { sessions: {} }),
+ reducedSessions = [];
+
+ for (let id in game.sessions) {
+ const reduced = Object.assign({}, game.sessions[id]);
+ if (reduced.player) {
+ delete reduced.player;
+ }
+ if (reduced.ws) {
+ delete reduced.ws;
+ }
+ if (reduced.keepAlive) {
+ delete reduced.keepAlive;
+ }
+ reducedGame.sessions[id] = reduced;
+
+ /* Do not send session-id as those are secrets */
+ 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);
+ });
+}
+
router.ws("/ws/:id", async (ws, req) => {
const { id } = req.params;
-
+ const gameId = id;
ws.id = req.session.player_id;
+ console.log(`${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`);
@@ -2807,7 +2873,7 @@ router.ws("/ws/:id", async (ws, req) => {
* we may miss the first messages from clients */
ws.on('error', async (event) => {
console.error(`WebSocket error: `, event.message);
- const game = await loadGame(id);
+ const game = await loadGame(gameId);
if (game) {
const session = getSession(game, req.session);
if (session && session.ws) {
@@ -2819,14 +2885,14 @@ router.ws("/ws/:id", async (ws, req) => {
ws.on('open', async (event) => {
console.log(`WebSocket open: `, event.message);
- const game = await loadGame(id);
+ const game = await loadGame(gameId);
if (game) {
resetDisconnectCheck(game, req);
}
});
ws.on('close', async (event) => {
- const game = await loadGame(id);
+ const game = await loadGame(gameId);
if (game) {
const session = getSession(game, req.session);
if (session && session.ws) {
@@ -2836,7 +2902,7 @@ router.ws("/ws/:id", async (ws, req) => {
}
session.ws.close();
session.ws = undefined;
- console.log(`WebSocket closed for ${session.name}`);
+ console.log(`WebSocket closed for ${getName(session)}`);
}
}
@@ -2844,8 +2910,14 @@ router.ws("/ws/:id", async (ws, req) => {
});
ws.on('message', async (message) => {
- const data = JSON.parse(message);
- const game = await loadGame(id);
+ let data;
+ try {
+ data = JSON.parse(message);
+ } catch (error) {
+ console.error(error, message);
+ return;
+ }
+ const game = await loadGame(gameId);
if (!game) {
console.error(`Unable to load/create new game for WS request.`);
return;
@@ -2860,6 +2932,8 @@ router.ws("/ws/:id", async (ws, req) => {
session.ws = ws;
}
+ let error = '', update;
+
switch (data.type) {
case 'join':
join(audio[id], session, id);
@@ -2876,14 +2950,16 @@ router.ws("/ws/:id", async (ws, req) => {
}
const { peer_id, ice_candidate } = data.config;
- console.log(`${id} - relayICECandidate ${session.name} to ${peer_id}`,
+ console.log(`${id} - relayICECandidate ${getName(session)} to ${peer_id}`,
ice_candidate);
+ message = JSON.stringify({
+ type: 'iceCandidate',
+ data: {'peer_id': getName(session), 'ice_candidate': ice_candidate }
+ });
+
if (peer_id in audio[id]) {
- audio[id][peer_id].send(JSON.stringify({
- type: 'iceCandidate',
- data: {'peer_id': session.name, 'ice_candidate': ice_candidate }
- }));
+ audio[id][peer_id].send(message);
}
} break;
@@ -2893,37 +2969,164 @@ router.ws("/ws/:id", async (ws, req) => {
return;
}
const { peer_id, session_description } = data.config;
- console.log(`${id} - relaySessionDescription ${session.name} to ${peer_id}`,
+ console.log(`${id} - relaySessionDescription ${getName(session)} to ${peer_id}`,
session_description);
+ message = JSON.stringify({
+ type: 'sessionDescription',
+ data: {'peer_id': getName(session), 'session_description': session_description }
+ });
if (peer_id in audio[id]) {
- audio[id][peer_id].send(JSON.stringify({
- type: 'sessionDescription',
- data: {'peer_id': session.name, 'session_description': session_description }
- }));
+ audio[id][peer_id].send(message);
}
} break;
case 'pong':
resetDisconnectCheck(game, req);
break;
+
case 'game-update':
- console.log(`Player ${session.name ? session.name : 'Unnamed'} requested a game update.`);
- resetDisconnectCheck(game, req);
- sendGame(req, undefined, game, undefined, ws);
- break;
+ console.log(`Player ${getName(session)} requested a game update.`);
+ message = JSON.stringify({
+ type: 'game-update',
+ update: filterGameForPlayer(game, session)
+ });
+ session.ws.send(message);
+ break;
+
+ case 'player-name':
+ console.log(`${id}:${getName(session)} - setPlayerName - ${data.name}`)
+ error = setPlayerName(game, session, data.name);
+ if (error) {
+ session.ws.send(JSON.stringify({ error }));
+ break;
+ }
+ update = {};
+
+ session.name = data.name;
+ update.name = session.name
+ if (session.color && session.color in game.players) {
+ game.players[session.color].name = session.name;
+ update.players = game.players;
+ }
+
+ 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;
+
+ case 'set':
+ console.log(`${id}:${getName(session)} - ${data.type}`);
+ update = {};
+ switch (data.field) {
+ case 'color':
+ error = setPlayerColor(game, session, data.value);
+ if (error) {
+ session.ws.send(JSON.stringify({ error }));
+ break;
+ }
+ 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: ${field}`);
+ break;
+ }
+ break;
+
+ case 'get':
+ console.log(`${id}:${getName(session)} - ${data.type}`);
+ update = {};
+ data.fields.forEach((field) => {
+ switch (field) {
+ case 'chat':
+ case 'startTime':
+ case 'state':
+ update[field] = game[field];
+ 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];
+ }
+ break;
+ default:
+ console.warn(`WARNING: Requested GET unsupported field: ${field}`);
+ if (field in game) {
+ update[field] = game.field;
+ }
+ break;
+ }
+ });
+ message = JSON.stringify({
+ type: 'game-update',
+ update
+ });
+ session.ws.send(message);
+ break;
+
+ case 'chat':
+ console.log(`${id}:${session.id} - ${data.type} - ${data.message}`)
+
+ /* Update the chat array */
+ addChatMessage(game, session, `${session.name}: ${data.message}`);
+
+ /* Send the update to all players */
+ message = JSON.stringify({
+ type: 'game-update',
+ update: {
+ chat: game.chat
+ }
+ });
+ for (let key in game.sessions) {
+ const _session = game.sessions[key];
+ if (!_session.ws) {
+ continue;
+ }
+ _session.ws.send(message);
+ }
+
+ /* Save the current game state to disk */
+ await saveGame(game);
+ break;
}
});
/* 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);
+ 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) {
+ console.error(`Session should never be empty after getSession`,
+ game, req.session);
+ }
resetDisconnectCheck(game, req);
@@ -2941,20 +3144,6 @@ router.ws("/ws/:id", async (ws, req) => {
}
});
-router.get("/:id", async (req, res/*, next*/) => {
- const { id } = req.params;
-// console.log("GET games/" + id);
-
- let game = await loadGame(id);
- if (game) {
- return sendGame(req, res, game)
- }
-
- game = createGame(id);
-
- return sendGame(req, res, game);
-});
-
const debugChat = (game, preamble) => {
preamble = `Degug ${preamble.trim()}`;
@@ -3026,7 +3215,7 @@ const sendGameToSession = (session, reducedSessions, game, reducedGame, error, r
if (!session.ws) {
console.error(`No WebSocket connection to ${session.name}`);
} else {
- console.log(`Sending update to ${session.name}`);
+ console.log(`Sending update to ${session.id}:${session.name ? session.name : 'Unnamed'}`);
session.ws.send(JSON.stringify({
type: 'game-update',
update: playerGame
@@ -3064,15 +3253,6 @@ const sendGame = async (req, res, game, error, wsUpdate) => {
};
}
- /* Ensure chat messages have a unique date: stamp as it is used as the index key */
- let lastTime = 0;
- if (game.chat) game.chat.forEach((message) => {
- if (message.date <= lastTime) {
- message.date = lastTime + 1;
- }
- lastTime = message.date;
- });
-
/* Calculate points and determine if there is a winner */
for (let key in game.players) {
const player = game.players[key];
@@ -3177,6 +3357,110 @@ const sendGame = async (req, res, game, error, wsUpdate) => {
}
}
+
+const filterGameForPlayer = (game, session) => {
+ const active = getActiveCount(game);
+
+ game.active = active;
+
+ /* Calculate points and determine if there is a winner */
+ for (let key in game.players) {
+ const player = game.players[key];
+ if (player.status === 'Not active') {
+ continue;
+ }
+ player.points = 0;
+ if (key === game.longestRoad) {
+ player.points += 2;
+ }
+ if (key === game.largestArmy) {
+ player.points += 2;
+ }
+ player.points += MAX_SETTLEMENTS - player.settlements;
+ player.points += 2 * (MAX_CITIES - player.cities);
+
+ player.unplayed = 0;
+ player.potential = 0;
+ player.development.forEach(card => {
+ if (card.type === 'vp') {
+ if (card.played) {
+ player.points++;
+ } else {
+ player.potential++;
+ }
+ }
+ if (!card.played) {
+ player.unplayed++;
+ }
+ });
+
+ console.log('TODO: Move game win state to card play section');
+ if (!game.winner && (player.points >= 10 && session.color === key)) {
+ game.winner = key;
+ game.state = 'winner';
+ delete game.turn.roll;
+ }
+ }
+
+ /* If the game isn't in a win state, do not share development card information
+ * with other players */
+ if (game.state !== 'winner') {
+ for (let key in game.players) {
+ const player = game.players[key];
+ if (player.status === 'Not active') {
+ continue;
+ }
+ delete player.potential;
+ }
+ }
+
+ /* Shallow copy game, filling its sessions with a shallow copy of
+ * sessions so we can then delete the player field from them */
+ const reducedGame = Object.assign({}, game, { sessions: {} }),
+ reducedSessions = [];
+
+ for (let id in game.sessions) {
+ const reduced = Object.assign({}, game.sessions[id]);
+ if (reduced.player) {
+ delete reduced.player;
+ }
+ if (reduced.ws) {
+ delete reduced.ws;
+ }
+ if (reduced.keepAlive) {
+ delete reduced.keepAlive;
+ }
+ reducedGame.sessions[id] = reduced;
+
+ /* Do not send session-id as those are secrets */
+ reducedSessions.push(reduced);
+ }
+
+ 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;
+
+ return Object.assign(reducedGame, {
+ timestamp: Date.now(),
+ status: session.error ? session.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
+ });
+}
+
const robberSteal = (game, color, type) => {
if (!game.stolen) {
game.stolen = {};
@@ -3330,18 +3614,13 @@ const createGame = (id) => {
return game;
};
-router.post("/:id?", (req, res/*, next*/) => {
+router.post("/", (req, res/*, next*/) => {
console.log("POST games/");
- const { id } = req.params;
- if (id && id in games) {
- const error = `Can not create new game for ${id} -- it already exists.`
- console.error(error);
- return res.status(400).send(error);
- }
+ const game = createGame();
- const game = createGame(id);
+ saveGame(game);
- return sendGame(req, res, game);
+ return res.status(200).send(filterGameForPlayer(game, session));
});
const setBeginnerGame = (game) => {