1
0

Turn notice!

Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
This commit is contained in:
James Ketrenos 2022-03-15 11:50:34 -07:00
parent 083e3c3a6f
commit 391e438990
6 changed files with 311 additions and 227 deletions

View File

@ -25,9 +25,3 @@
align-self: stretch; align-self: stretch;
} }
.Actions button {
margin: 0.25em;
background-color: white;
border: 1px solid black !important;
}

View File

@ -19,7 +19,7 @@ body {
background-image: url("./assets/tabletop.png"); background-image: url("./assets/tabletop.png");
} }
.Table .ErrorDialog { .Table .Dialogs {
z-index: 10000; z-index: 10000;
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
@ -29,46 +29,44 @@ body {
left: 0; left: 0;
bottom: 0; bottom: 0;
right: 0; right: 0;
}
.Table .Dialogs .Dialog {
display: flex;
position: absolute;
flex-shrink: 1;
flex-direction: column;
padding: 0.25rem;
left: 0;
right: 0;
top: 0;
bottom: 0;
justify-content: space-around;
align-items: center;
}
.Table .Dialogs .Dialog > div {
display: flex;
padding: 1rem;
flex-direction: column;
}
.Table .Dialogs .Dialog > div > div:first-child {
padding: 1rem;
}
.Table .Dialogs .TurnNoticeDialog {
background-color: #7a680060;
}
.Table .Dialogs .ErrorDialog {
background-color: #40000060; background-color: #40000060;
} }
.Table .ErrorDialog .Error { .Table .Dialogs .WarningDialog {
display: flex;
flex-direction: column;
padding: 0.25rem;
}
.Table .ErrorDialog .Error > div {
display: flex;
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;
background-color: #00000060; background-color: #00000060;
} }
.Table .WarningDialog .Warning {
display: flex;
flex-direction: column;
padding: 0.25rem;
}
.Table .WarningDialog .Warning > div {
display: flex;
padding: 1rem;
}
.Table .PlayersStatus { .Table .PlayersStatus {
z-index: 5000; z-index: 5000;
} }
@ -161,3 +159,15 @@ body {
right: 0; right: 0;
bottom: 0; bottom: 0;
} }
.Table button {
margin: 0.25em;
background-color: white;
border: 1px solid black !important;
}
.Table button:disabled {
opacity: 0.5;
border: 1px solid #ccc !important;
}

View File

@ -29,6 +29,7 @@ import { Winner } from "./Winner.js";
import history from "./history.js"; import history from "./history.js";
import "./App.css"; import "./App.css";
import equal from "fast-deep-equal";
const Table = () => { const Table = () => {
const params = useParams(); const params = useParams();
@ -39,14 +40,14 @@ const Table = () => {
const [ warning, setWarning ] = useState(undefined); const [ warning, setWarning ] = useState(undefined);
const [ peers, setPeers ] = useState({}); const [ peers, setPeers ] = useState({});
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
const [connecting, setConnecting] = useState(undefined); const [connection, setConnection] = useState(undefined);
const [state, setState] = useState(undefined); const [state, setState] = useState(undefined);
const [color, setColor] = useState(undefined); const [color, setColor] = useState(undefined);
const [players, setPlayers] = useState(undefined); const [setPlayers] = useState(undefined);
const [player, setPlayer] = useState(undefined); const [priv, setPriv] = useState(undefined);
const [buildActive, setBuildActive] = useState(false); const [buildActive, setBuildActive] = useState(false);
const [cardActive, setCardActive] = useState(undefined); const [cardActive, setCardActive] = useState(undefined);
const fields = [ 'id', 'state', 'color', 'name' ]; const fields = [ 'id', 'state', 'color', 'name', 'private' ];
useEffect(() => { useEffect(() => {
console.log(`app - media-agent - peers`, peers); console.log(`app - media-agent - peers`, peers);
@ -58,7 +59,7 @@ const Table = () => {
/* We do not set the socket as connected until the 'open' message /* We do not set the socket as connected until the 'open' message
* comes through */ * comes through */
setConnecting(event.target); setConnection(ws);
/* Request a full game-update /* Request a full game-update
* We only need gameId and name for App.js, however in the event * We only need gameId and name for App.js, however in the event
@ -95,31 +96,22 @@ const Table = () => {
setLoaded(true); setLoaded(true);
} }
console.log(`ws: message - ${data.type}`, data.update); console.log(`ws: message - ${data.type}`, data.update);
if ('player' in data.update) {
const player = data.update.player; if ('private' in data.update && !equal(priv, data.update.private)) {
if (player.name !== name) { const priv = data.update.private;
console.log(`App - setting name (via player): ${data.update.name}`); if (priv.name !== name) {
setName(data.update.name); console.log(`App - setting name (via private): ${priv.name}`);
setName(priv.name);
} }
if (player.color !== color) { if (priv.color !== color) {
console.log(`App - setting color (via player): ${data.update.color}`); console.log(`App - setting color (via private): ${priv.color}`);
setColor(data.update.color); setColor(priv.color);
} }
setPriv(priv);
} }
if ('players' in data.update) { if ('players' in data.update) {
setPlayers(data.update.players); setPlayers(data.update.players);
if (color in data.update.players) {
if (player !== data.update.players[color]) {
setPlayer(data.update.players[color]);
}
} else {
if (player) {
setPlayer(undefined);
}
if (color) {
setColor(undefined);
}
}
} }
if ('name' in data.update && data.update.name !== name) { if ('name' in data.update && data.update.name !== name) {
console.log(`App - setting name: ${data.update.name}`); console.log(`App - setting name: ${data.update.name}`);
@ -136,21 +128,21 @@ const Table = () => {
if ('color' in data.update && data.update.color !== color) { if ('color' in data.update && data.update.color !== color) {
console.log(`App - setting color: ${color}`); console.log(`App - setting color: ${color}`);
setColor(data.update.color); setColor(data.update.color);
if (players && players[data.update.color] !== player) {
setPlayer(players[data.update.color]);
}
} }
break; break;
default: default:
break; break;
} }
}; };
const sendUpdate = (update) => {
ws.send(JSON.stringify(update));
};
const cbResetConnection = useCallback(() => { const cbResetConnection = useCallback(() => {
let timer = 0; let timer = 0;
function reset() { function reset() {
timer = 0; timer = 0;
setConnecting(undefined); setConnection(undefined);
}; };
return _ => { return _ => {
if (timer) { if (timer) {
@ -158,7 +150,7 @@ const Table = () => {
} }
timer = setTimeout(reset, 5000); timer = setTimeout(reset, 5000);
}; };
}, [setConnecting]); }, [setConnection]);
const resetConnection = cbResetConnection(); const resetConnection = cbResetConnection();
@ -252,7 +244,7 @@ const Table = () => {
console.log(`table - bind`); console.log(`table - bind`);
if (!ws && !connecting) { if (!ws && !connection) {
let loc = window.location, new_uri; let loc = window.location, new_uri;
if (loc.protocol === "https:") { if (loc.protocol === "https:") {
new_uri = "wss"; new_uri = "wss";
@ -262,7 +254,7 @@ const Table = () => {
new_uri = `${new_uri}://${loc.host}${base}/api/v1/games/ws/${gameId}`; new_uri = `${new_uri}://${loc.host}${base}/api/v1/games/ws/${gameId}`;
console.log(`Attempting WebSocket connection to ${new_uri}`); console.log(`Attempting WebSocket connection to ${new_uri}`);
setWs(new WebSocket(new_uri)); setWs(new WebSocket(new_uri));
setConnecting(undefined); setConnection(undefined);
return unbind; return unbind;
} }
@ -287,9 +279,9 @@ const Table = () => {
ws.removeEventListener('error', cbError); ws.removeEventListener('error', cbError);
ws.removeEventListener('message', cbMessage); ws.removeEventListener('message', cbMessage);
} }
}, [ setWs, connecting, setConnecting, gameId, ws, refWsOpen, refWsMessage, refWsClose, refWsError ]); }, [ setWs, connection, setConnection, gameId, ws, refWsOpen, refWsMessage, refWsClose, refWsError ]);
return <GlobalContext.Provider value={{ ws: connecting, name, gameId, peers, setPeers }}> return <GlobalContext.Provider value={{ ws: connection, name, gameId, peers, setPeers }}>
<MediaAgent/> <MediaAgent/>
{ /* <PingPong/> */ } { /* <PingPong/> */ }
<div className="Table"> <div className="Table">
@ -297,13 +289,20 @@ const Table = () => {
<Trade/> <Trade/>
<div className="Game"> <div className="Game">
<div className="Dialogs"> <div className="Dialogs">
{ error && <div className="ErrorDialog"> { error && <div className="Dialog ErrorDialog">
<Paper className="Error"> <Paper className="Error">
<div>{ error }</div> <div>{ error }</div>
<Button onClick={() => { setError("")}}>dismiss</Button> <Button onClick={() => { setError("")}}>dismiss</Button>
</Paper> </Paper>
</div> } </div> }
{ warning && <div className="WarningDialog"> { priv && priv.turnNotice && <div className="Dialog TurnNoticeDialog">
<Paper className="TurnNotice">
<div>{ priv.turnNotice }</div>
<Button onClick={() => { sendUpdate({type: 'pass'}) }}>done</Button>
<Button onClick={() => { sendUpdate({type: 'turn-notice'}) }}>dismiss</Button>
</Paper>
</div> }
{ warning && <div className="Dialog WarningDialog">
<Paper className="Warning"> <Paper className="Warning">
<div>{ warning }</div> <div>{ warning }</div>
<Button onClick={() => { setWarning("")}}>dismiss</Button> <Button onClick={() => { setWarning("")}}>dismiss</Button>

View File

@ -114,7 +114,6 @@
height: 50px; height: 50px;
transform-origin: 50% 0; transform-origin: 50% 0;
z-index: 11; /* Above Tile, below Corner */ z-index: 11; /* Above Tile, below Corner */
pointer-events: none;
} }
.Road-Shape { .Road-Shape {
@ -153,47 +152,42 @@
pointer-events: none; pointer-events: none;
} }
@keyframes blink {
from {
filter: brightness(90%) drop-shadow(0 0 5px black);
opacity: 0.8;
}
to {
filter: brightness(100%) drop-shadow(0 0 10px black);
opacity: 1;
}
}
.Board .Option { .Board .Option {
cursor: pointer; cursor: pointer;
pointer-events: all; pointer-events: all;
filter: brightness(100%) drop-shadow(0 0 10px black); filter: brightness(150%) drop-shadow(0 0 10px black);
/*
opacity: 0.7;
transition-timing-function: ease-in-out; transition-timing-function: ease-in-out;
animation-duration: 0.5s; animation-duration: 0.5s;
animation-name: blink; animation-name: blink;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-direction: alternate; animation-direction: alternate;
*/
} }
.Tile-Shape:hover, .Tile-Shape:hover,
.Corner-Shape:hover, .Corner-Shape:hover,
.Road-Shape:hover { .Road-Shape:hover {
background-color: white; background-color: white !important;
} }
.Board .Option .Tile-Shape, .Board .Option .Tile-Shape,
.Board .Option .Corner-Shape, .Board .Option .Corner-Shape,
.Board .Option .Road-Shape { .Board .Option .Road-Shape {
background-color: rgba(255, 255, 255, 0.75); background-color: rgb(185, 185, 185);
} }
.Board .Option:hover {
filter: brightness(150%) drop-shadow(0 0 5px white);
}
.Board .Option:hover {
/* opacity: 1;
filter: brightness(150%) drop-shadow(0 0 2px black);
animation-play-state: paused;
*/
}
.Robber .Pip-Shape { .Robber .Pip-Shape {
top: -40px; top: -40px;
left: -40px; left: -40px;
@ -303,44 +297,4 @@
background-color: rgba(192, 192, 255, 0.8); background-color: rgba(192, 192, 255, 0.8);
} }
*/ */
/*
@keyframes blink {
from {
filter: brightness(90%);
opacity: 1;
}
50% {
filter: brightness(150%);
}
to {
filter: brightness(150%);
opacity: 0.5;
}
}
*/
.Board .Option:hover {
/* opacity: 1;
animation-play-state: paused;
*/
}
.Board .Option {
/*
opacity: 0.7;
transition-timing-function: ease-in-out;
animation-duration: 0.5s;
animation-name: blink;
animation-iteration-count: infinite;
animation-direction: alternate;
*/
}

View File

@ -24,6 +24,8 @@ const
borderImageWidth = (2 + 2/3) * tileImageWidth, /* 2.667 * .Tile.width */ borderImageWidth = (2 + 2/3) * tileImageWidth, /* 2.667 * .Tile.width */
borderImageHeight = borderImageWidth * 0.29; /* 0.29 * .Border.height */ borderImageHeight = borderImageWidth * 0.29; /* 0.29 * .Border.height */
let hack = undefined;
const Board = () => { const Board = () => {
const { ws } = useContext(GlobalContext); const { ws } = useContext(GlobalContext);
const board = useRef(); const board = useRef();
@ -57,6 +59,13 @@ const Board = () => {
console.log(`board - render ws is ${!ws ? 'NULL' : (ws.readyState === ws.OPEN ? 'OPEN' : '!OPEN')}`); console.log(`board - render ws is ${!ws ? 'NULL' : (ws.readyState === ws.OPEN ? 'OPEN' : '!OPEN')}`);
const onWsMessage = (event) => { const onWsMessage = (event) => {
if (ws !== event.target) {
console.error(`Disconnect occur?`);
}
if (hack !== ws) {
console.error(`Setting hack`)
hack = ws;
}
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
switch (data.type) { switch (data.type) {
case 'game-update': case 'game-update':
@ -131,10 +140,12 @@ const Board = () => {
break; break;
} }
}; };
const refWs = useRef(ws);
const refWsMessage = useRef(onWsMessage); const refWsMessage = useRef(onWsMessage);
useEffect(() => { refWsMessage.current = onWsMessage; }); useEffect(() => { refWsMessage.current = onWsMessage; });
useEffect(() => { useEffect(() => {
if (!ws) { return; } if (!ws) { return; }
refWs.current = ws;
console.log('board - bind'); console.log('board - bind');
const cbMessage = e => refWsMessage.current(e); const cbMessage = e => refWsMessage.current(e);
ws.addEventListener('message', cbMessage); ws.addEventListener('message', cbMessage);
@ -195,7 +206,6 @@ const Board = () => {
onResize(); onResize();
const Tile = ({tile}) => { const Tile = ({tile}) => {
const onClick = (event) => { const onClick = (event) => {
console.log(`Tile clicked: ${tile.index}`); console.log(`Tile clicked: ${tile.index}`);
@ -214,7 +224,8 @@ const Board = () => {
}} }}
><div className="Tile-Shape"/></div>; ><div className="Tile-Shape"/></div>;
}; };
/*
useRef didn't work...
const staticSendCallback = (type, index) => { const staticSendCallback = (type, index) => {
ws.send(JSON.stringify({ ws.send(JSON.stringify({
type, index type, index
@ -222,14 +233,40 @@ const Board = () => {
}; };
const refStaticSendCallback = useRef(staticSendCallback); const refStaticSendCallback = useRef(staticSendCallback);
useEffect(() => { refStaticSendCallback.current = staticSendCallback; }); useEffect(() => { refStaticSendCallback.current = staticSendCallback; });
const sendPlacement = refStaticSendCallback.current;
const sendPlacement = function(...) doesn't work.
*/
/*
* const sendPlacement = useCallabck(...) doesn't work.
*
* However it is required for dependency tracking.
*/
const sendPlacement = useCallback((type, index) => {
console.log(`board - sendPlacement - ws is ${!ws ? 'NULL' : (ws.readyState === ws.OPEN ? 'OPEN' : '!OPEN')}`);
if (ws.readyState !== ws.OPEN) {
console.error(`ws is not OPEN in sendPlacement`);
}
if (ws !== hack) {
console.error(`hack and ws are different!`);
if (refWs.current === hack) {
console.log(`refWs is correct!`);
}
}
refWs.current.send(JSON.stringify({
type, index
}));
}, [ws, refWs]);
const onRoadClicked = useCallback((road) => { const onRoadClicked = useCallback((road) => {
console.log(`Road clicked: ${road.index}`); console.log(`Road clicked: ${road.index}`);
sendPlacement('place-road', road.index); sendPlacement('place-road', road.index);
}, [sendPlacement]); }, [sendPlacement]);
const Road = ({road}) => { const Road = useCallback(({road}) => {
return <div className="Road" return <div className="Road"
onClick={() => { onRoadClicked(road) }} onClick={() => { onRoadClicked(road) }}
data-index={road.index} data-index={road.index}
@ -239,7 +276,7 @@ const Board = () => {
left: `${road.left}px` left: `${road.left}px`
}} }}
><div className="Road-Shape"/></div>; ><div className="Road-Shape"/></div>;
}; }, [onRoadClicked]);
const onCornerClicked = useCallback((event, corner) => { const onCornerClicked = useCallback((event, corner) => {
let type; let type;

View File

@ -103,12 +103,6 @@ const games = {};
const audio = {}; const audio = {};
const processTies = (players) => { const processTies = (players) => {
players.sort((A, B) => {
if (A.order === B.order) {
return B.orderRoll - A.orderRoll;
}
return B.order - A.order;
});
/* Sort the players into buckets based on their /* Sort the players into buckets based on their
* order, and their current roll. If a resulting * order, and their current roll. If a resulting
@ -119,69 +113,87 @@ const processTies = (players) => {
if (!slots[player.order]) { if (!slots[player.order]) {
slots[player.order] = []; slots[player.order] = [];
} }
if (!(player.orderRoll in slots[player.order])) { slots[player.order].push(player);
slots[player.order][player.orderRoll] = [];
}
slots[player.order][player.orderRoll].push(player);
}); });
let ties = false, order = 0; let ties = false, position = 1;
const irstify = (position) => {
switch (position) {
case 1: return `1st`;
case 2: return `2nd`;
case 3: return `3rd`;
case 4: return `4th`;
default: return position;
}
}
console.log(`Slots: `, slots);
/* Reverse from high to low */ /* Reverse from high to low */
slots.reverse().forEach((slot) => { slots.reverse().forEach((slot) => {
slot.forEach(dice => { if (slot.length !== 1) {
if (dice.length !== 1) { ties = true;
ties = true; slot.forEach(player => {
dice.forEach(player => { player.orderRoll = 0; /* Ties have to be re-rolled */
player.orderRoll = 0; player.position = irstify(position);
player.order = order; player.orderStatus = `Tied for ${irstify(position)}`;
player.orderStatus = `Tied.`; player.tied = true;
player.tied = true; });
}); } else {
} else { slot[0].tied = false;
dice[0].order = order; slot[0].position = irstify(position);
dice[0].tied = false; slot[0].orderStatus = `Placed in ${irstify(position)}.`;
dice[0].orderStatus = `Placed in ${order+1}.`; }
} position += slot.length
order += dice.length
})
}); });
return !ties; return ties;
} }
const processGameOrder = (game, player, dice) => { const processGameOrder = (game, player, dice) => {
if (player.orderRoll) { if (player.orderRoll) {
return `You have already rolled for game order and are not in a tie.`; return `You have already rolled for game order and are not in a tie.`;
} }
player.orderRoll = dice; player.orderRoll = dice;
player.order = player.order * 6 + dice;
const players = []; const players = [];
let doneRolling = true; let doneRolling = true;
for (let key in game.players) { for (let key in game.players) {
const tmp = game.players[key]; if (!game.players[key].orderRoll) {
if (tmp.status === 'Not active') {
continue;
}
if (!tmp.orderRoll) {
doneRolling = false; doneRolling = false;
} }
players.push(tmp); players.push(game.players[key]);
} }
/* If 'doneRolling' is FALSE then there are still players to roll */ /* If 'doneRolling' is FALSE then there are still players to roll */
if (!doneRolling || !processTies(players)) { if (!doneRolling) {
sendUpdateToPlayers(game, { sendUpdateToPlayers(game, {
players: getFilteredPlayers(game), players: getFilteredPlayers(game),
chat: game.chat chat: game.chat
}) })
return; return;
} }
/* sort updated player.order into the array */
players.sort((A, B) => {
return B.order - A.order;
});
console.log(`Pre process ties: `, players);
addChatMessage(game, null, `Player order set to ${players if (processTies(players)) {
.map((player, index) => { console.log(`${info}: There are ties in player rolls:`, players);
return `${index+1}. ${player.name}`; sendUpdateToPlayers(game, {
}).join(', ')}.`); players: getFilteredPlayers(game),
chat: game.chat
});
return;
}
addChatMessage(game, null, `Player order set to ` +
players.map((player) => `${player.position}: ${player.name}`)
.join(', ') + `.`);
game.playerOrder = players.map(player => player.color); game.playerOrder = players.map(player => player.color);
game.state = 'initial-placement'; game.state = 'initial-placement';
@ -210,18 +222,13 @@ const roll = (game, session) => {
name = session.name ? session.name : "Unnamed"; name = session.name ? session.name : "Unnamed";
switch (game.state) { switch (game.state) {
case "lobby": case "lobby": /* currently not available as roll is only after color is
* set for players */
addChatMessage(game, session, `${name} rolled ${Math.ceil(Math.random() * 6)}.`); addChatMessage(game, session, `${name} rolled ${Math.ceil(Math.random() * 6)}.`);
sendUpdateToPlayers(game, { chat: game.chat }); sendUpdateToPlayers(game, { chat: game.chat });
return; return;
case "game-order": case "game-order":
if (!player) {
return `This player is not active!`;
}
if (player.order && player.orderRoll) {
return `Player ${name} has already rolled for player order.`;
}
game.startTime = Date.now(); game.startTime = Date.now();
const dice = Math.ceil(Math.random() * 6); const dice = Math.ceil(Math.random() * 6);
addChatMessage(game, session, `${name} rolled ${dice}.`); addChatMessage(game, session, `${name} rolled ${dice}.`);
@ -730,14 +737,15 @@ const adminActions = (game, action, value) => {
case "pass": case "pass":
let name = game.turn.name; let name = game.turn.name;
const next = getNextPlayer(game, name); const next = getNextPlayerSession(game, name);
game.turn = { game.turn = {
name: next.player, name: next.player,
color: next.color color: next.color
}; };
game.turns++; game.turns++;
startTurnTimer(game, next);
addChatMessage(game, null, `The admin skipped ${name}'s turn.`); addChatMessage(game, null, `The admin skipped ${name}'s turn.`);
addChatMessage(game, null, `It is ${next}'s turn.`); addChatMessage(game, null, `It is ${next.name}'s turn.`);
break; break;
case "kick": case "kick":
@ -1065,7 +1073,7 @@ const getFirstPlayerName = (game) => {
return ''; return '';
} }
const getNextPlayer = (game, name) => { const getNextPlayerSession = (game, name) => {
let color; let color;
for (let id in game.sessions) { for (let id in game.sessions) {
if (game.sessions[id].name === name) { if (game.sessions[id].name === name) {
@ -1073,25 +1081,20 @@ const getNextPlayer = (game, name) => {
break; break;
} }
} }
console.log(`current player is ${color}`);
let index = game.playerOrder.indexOf(color); let index = game.playerOrder.indexOf(color);
console.log(`current player is ${color} ${index}`);
index = (index + 1) % game.playerOrder.length; index = (index + 1) % game.playerOrder.length;
console.log(`current player is ${color} ${index}`);
color = game.playerOrder[index]; color = game.playerOrder[index];
console.log(`current player is ${color} ${index}`);
for (let id in game.sessions) { for (let id in game.sessions) {
if (game.sessions[id].color === color) { if (game.sessions[id].color === color) {
return game.sessions[id].player; return game.sessions[id];
} }
} }
console.log(`nothing found... returning current player.`); console.error(`getNextPlayerSession -- no player found!`);
console.log(game.players); console.log(game.players);
return player;
} }
const getPrevPlayer = (game, name) => { const getPrevPlayerSession = (game, name) => {
let color; let color;
for (let id in game.sessions) { for (let id in game.sessions) {
if (game.sessions[id].name === name) { if (game.sessions[id].name === name) {
@ -1103,10 +1106,11 @@ const getPrevPlayer = (game, name) => {
index = (index - 1) % game.playerOrder.length; index = (index - 1) % game.playerOrder.length;
for (let id in game.sessions) { for (let id in game.sessions) {
if (game.sessions[id].color === game.playerOrder[index]) { if (game.sessions[id].color === game.playerOrder[index]) {
return game.sessions[id].player; return game.sessions[id];
} }
} }
return player; console.error(`getNextPlayerSession -- no player found!`);
console.log(game.players);
} }
const processCorner = (game, color, cornerIndex, placedCorner) => { const processCorner = (game, color, cornerIndex, placedCorner) => {
@ -1834,6 +1838,44 @@ const trade = (game, session, action, offer) => {
} }
} }
const clearTimeNotice= (game, session) => {
if (!session.player.turnNotice) {
return `You have not been idle.`;
}
session.player.turnNotice = "";
sendUpdateToPlayer(game, session, {
private: session.player
});
};
const startTurnTimer = (game, session) => {
const timeout = 30;
console.log(`${session.id}: (Re)setting turn timer for ${session.name} to ${timeout} seconds.`);
if (game.turnTimer) {
clearTimeout(game.turnTimer);
}
game.turnTimer = setTimeout(() => {
console.log(`${session.id}: Turn timer expired for ${session.name}`);
session.player.turnNotice = 'It is still your turn.';
sendUpdateToPlayer(game, session, {
private: session.player
});
resetTurnTimer(game, session);
}, timeout * 1000);
}
const resetTurnTimer = (game, session) => {
startTurnTimer(game, session);
}
const stopTurnTimer = (game) => {
if (game.turnTimer) {
console.log(`${info}: Stopping turn timer.`);
clearTimeout(game.turnTimer);
game.turnTimer = 0;
}
}
const shuffle = (game, session) => { const shuffle = (game, session) => {
const name = session.name; const name = session.name;
@ -1861,17 +1903,21 @@ const pass = (game, session) => {
return `Robber is in action. Turn can not stop until all Robber tasks are resolved.`; return `Robber is in action. Turn can not stop until all Robber tasks are resolved.`;
} }
const next = getNextPlayer(game, session.name); const next = getNextPlayerSession(game, session.name);
session.player.totalTime += Date.now() - session.player.turnStart; session.player.totalTime += Date.now() - session.player.turnStart;
session.player.turnNotice = "";
game.turn = { game.turn = {
name: next.name, name: next.name,
color: next.color color: next.color
}; };
next.turnStart = Date.now(); next.turnStart = Date.now();
game.turnTimer = startTurnTimer(game, next);
game.turns++; game.turns++;
addActivity(game, session, `${name} passed their turn.`); addActivity(game, session, `${name} passed their turn.`);
addChatMessage(game, null, `It is ${next.name}'s turn.`); addChatMessage(game, null, `It is ${next.name}'s turn.`);
sendUpdateToPlayer(game, session, {
private: session.player
});
sendUpdateToPlayers(game, { sendUpdateToPlayers(game, {
turns: game.turns, turns: game.turns,
turn: game.turn, turn: game.turn,
@ -2406,11 +2452,9 @@ const placeRoad = (game, session, index) => {
delete game.direction; delete game.direction;
} else { } else {
if (game.direction === 'forward') { if (game.direction === 'forward') {
next = getNextPlayer(game, session.name); next = getNextPlayerSession(game, session.name);
console.log(`advance from ${session.name} to `, next);
} else { } else {
next = getPrevPlayer(game, session.name); next = getPrevPlayerSession(game, session.name);
console.log(`devance from ${session.name} to `, next);
} }
} }
if (next) { if (next) {
@ -2418,6 +2462,7 @@ const placeRoad = (game, session, index) => {
name: next.name, name: next.name,
color: next.color color: next.color
}; };
startTurnTimer(game, next);
setForSettlementPlacement(game, getValidCorners(game)); setForSettlementPlacement(game, getValidCorners(game));
calculateRoadLengths(game, session); calculateRoadLengths(game, session);
addChatMessage(game, null, `It is ${next.name}'s turn to place a settlement.`); addChatMessage(game, null, `It is ${next.name}'s turn to place a settlement.`);
@ -2863,6 +2908,14 @@ const setGameState = (game, session, state) => {
if (active < 2) { if (active < 2) {
return `You need at least two players to start the game.`; return `You need at least two players to start the game.`;
} }
/* Delete any non-played colors from the player map; reduces all
* code that would otherwise have to filter out players by checking
* the 'Not active' state of player.status */
for (let key in game.players) {
if (game.players[key].status !== 'Active') {
delete game.players[key];
}
}
addChatMessage(game, null, `${session.name} requested to start the game.`); addChatMessage(game, null, `${session.name} requested to start the game.`);
game.state = state; game.state = state;
@ -2968,12 +3021,14 @@ const saveGame = async (game) => {
if (reduced.keepAlive) { if (reduced.keepAlive) {
delete reduced.keepAlive; delete reduced.keepAlive;
} }
reducedGame.sessions[id] = reduced; reducedGame.sessions[id] = reduced;
/* Do not send session-id as those are secrets */ /* Do not send session-id as those are secrets */
reducedSessions.push(reduced); reducedSessions.push(reduced);
} }
delete reducedGame.turnTimer;
delete reducedGame.unselected; delete reducedGame.unselected;
/* Save per turn while debugging... */ /* Save per turn while debugging... */
@ -3062,15 +3117,15 @@ const sendUpdateToPlayers = async (game, update) => {
for (let key in game.sessions) { for (let key in game.sessions) {
const _session = game.sessions[key]; const _session = game.sessions[key];
if (!_session.ws) { if (!_session.ws) {
continue; console.log(`${_session.id}: -> sendUpdateToPlayers: Currently no connection.`);
} else {
_session.ws.send(message);
} }
_session.ws.send(message);
} }
await saveGame(game); await saveGame(game);
} }
const sendUpdateToPlayer = async (game, session, update) => { const sendUpdateToPlayer = async (game, session, update) => {
/* Ensure clearing of a field actually gets sent by setting /* Ensure clearing of a field actually gets sent by setting
* undefined to 'false' * undefined to 'false'
*/ */
@ -3093,7 +3148,12 @@ const sendUpdateToPlayer = async (game, session, update) => {
type: 'game-update', type: 'game-update',
update update
}); });
session.ws.send(message);
if (!session.ws) {
console.log(`${session.id}: -> sendUpdateToPlayer: Currently no connection.`);
} else {
session.ws.send(message);
}
} }
const getFilteredUnselected = (game) => { const getFilteredUnselected = (game) => {
@ -3216,6 +3276,7 @@ const calculatePoints = (game, update) => {
game.winner = update.winner; game.winner = update.winner;
game.state = 'winner'; game.state = 'winner';
game.waiting = []; game.waiting = [];
stopTurnTimer(game);
} }
} }
} }
@ -3463,7 +3524,8 @@ router.ws("/ws/:id", async (ws, req) => {
case 'startTime': case 'startTime':
case 'state': case 'state':
case 'turns': case 'turns':
update[field] = game[field]; case 'winner':
update[field] = game[field];
break; break;
case 'name': case 'name':
update.name = session.name; update.name = session.name;
@ -3509,13 +3571,6 @@ router.ws("/ws/:id", async (ws, req) => {
sendUpdateToPlayers(game, { chat: game.chat }); sendUpdateToPlayers(game, { chat: game.chat });
break; break;
case 'roll':
console.log(`${short}: <- roll:${getName(session)}`);
warning = roll(game, session);
if (warning) {
sendWarning(session, warning);
}
break;
default: default:
processed = false; processed = false;
break; break;
@ -3534,7 +3589,15 @@ router.ws("/ws/:id", async (ws, req) => {
return; return;
} }
processed = true;
switch (data.type) { switch (data.type) {
case 'roll':
console.log(`${short}: <- roll:${getName(session)}`);
warning = roll(game, session);
if (warning) {
sendWarning(session, warning);
}
break;
case 'shuffle': case 'shuffle':
console.log(`${short}: <- shuffle:${getName(session)}`); console.log(`${short}: <- shuffle:${getName(session)}`);
warning = shuffle(game, session); warning = shuffle(game, session);
@ -3634,7 +3697,7 @@ router.ws("/ws/:id", async (ws, req) => {
} }
break; break;
case 'trade': case 'trade':
console.log(`${short}: <- trade:${getName(session)} - ${data.action} - `, data.offer); console.log(`${short}: <- trade:${getName(session)} - ${data.action} - `, data.offer ? data.offer : 'no trade yet');
warning = trade(game, session, data.action, data.offer); warning = trade(game, session, data.action, data.offer);
if (warning) { if (warning) {
sendWarning(session, warning); sendWarning(session, warning);
@ -3658,6 +3721,13 @@ router.ws("/ws/:id", async (ws, req) => {
}); });
} }
break; break;
case 'turn-notice':
console.log(`${short}: <- turn-notice:${getName(session)}`);
warning = clearTimeNotice(game, session);
if (warning) {
sendWarning(session, warning);
}
break;
case 'goto-lobby': case 'goto-lobby':
console.log(`${short}: <- goto-lobby:${getName(session)}`); console.log(`${short}: <- goto-lobby:${getName(session)}`);
warning = gotoLobby(game, session); warning = gotoLobby(game, session);
@ -3667,10 +3737,16 @@ router.ws("/ws/:id", async (ws, req) => {
break; break;
default: default:
console.warn(`Unsupported request: ${data.type}`); console.warn(`Unsupported request: ${data.type}`);
processed = false;
break; break;
} }
if (processed && session.color === game.turn.color) {
resetTurnTimer(game, session);
}
}); });
/* This will result in the node tick moving forward; if we haven't already /* 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 * setup the event handlers, a 'message' could come through prior to this
* completing */ * completing */
@ -3694,6 +3770,11 @@ router.ws("/ws/:id", async (ws, req) => {
}); });
} }
/* If the current turn player just rejoined, set their turn timer */
if (game.turn && game.turn.color === session.color) {
resetTurnTimer(game, session);
}
if (session.name) { if (session.name) {
if (session.color) { if (session.color) {
addChatMessage(game, null, `${session.name} has reconnected to the game.`); addChatMessage(game, null, `${session.name} has reconnected to the game.`);
@ -3774,6 +3855,9 @@ const getFilteredGameForPlayer = (game, session) => {
/* Strip out data that should not be shared with players */ /* Strip out data that should not be shared with players */
delete reducedGame.developmentCards; delete reducedGame.developmentCards;
/* Delete the game timer */
delete reducedGame.turnTimer;
reducedGame.unselected = getFilteredUnselected(game); reducedGame.unselected = getFilteredUnselected(game);
return Object.assign(reducedGame, { return Object.assign(reducedGame, {
@ -3840,6 +3924,8 @@ const resetGame = (game) => {
active: 0 active: 0
}); });
stopTurnTimer(game);
/* Populate the game corner and road placement data as cleared */ /* Populate the game corner and road placement data as cleared */
for (let i = 0; i < layout.corners.length; i++) { for (let i = 0; i < layout.corners.length; i++) {
game.placements.corners[i] = { game.placements.corners[i] = {
@ -3885,10 +3971,14 @@ const resetGame = (game) => {
shuffleArray(game.developmentCards); shuffleArray(game.developmentCards);
/* Reset all player data */ /* Reset all player data, and add in any missing colors */
for (let color in game.players) { [ 'R', 'B', 'W', 'O' ].forEach(color => {
clearPlayer(game.players[color]); if (color in game.players) {
} clearPlayer(game.players[color]);
} else {
game.players.color = newPlayer(color);
}
});
/* Ensure sessions are connected to player objects */ /* Ensure sessions are connected to player objects */
for (let key in game.sessions) { for (let key in game.sessions) {