1
0
James Ketrenos c9ba74dfae Fix winner dialog not showing on multi-game sessions
Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
2022-05-24 14:55:49 -07:00

415 lines
13 KiB
JavaScript
Executable File

import React, { useState, useCallback, useEffect, useRef } from "react";
import {
BrowserRouter as Router,
Route,
Routes,
useParams
} from "react-router-dom";
import Paper from '@material-ui/core/Paper';
import Button from '@material-ui/core/Button';
import { GlobalContext } from "./GlobalContext.js";
//import { PingPong } from "./PingPong.js";
import { PlayerList } from "./PlayerList.js";
import { Chat } from "./Chat.js";
import { Board } from "./Board.js";
import { Actions } from "./Actions.js";
import { base, gamesPath } from './Common.js';
import { GameOrder } from "./GameOrder.js";
import { Activities } from "./Activities.js";
import { SelectPlayer } from "./SelectPlayer.js";
import { PlayersStatus } from "./PlayersStatus.js";
import { ViewCard } from "./ViewCard.js";
import { ChooseCard } from "./ChooseCard.js";
import { Hand } from "./Hand.js";
import { Trade } from "./Trade.js";
import { Winner } from "./Winner.js";
import { HouseRules } from "./HouseRules.js";
import history from "./history.js";
import "./App.css";
import equal from "fast-deep-equal";
const Table = () => {
const params = useParams();
const [ gameId, setGameId ] = useState(params.gameId ? params.gameId : undefined);
const [ ws, setWs ] = useState(); /* tracks full websocket lifetime */
const [connection, setConnection] = useState(undefined); /* set after ws is in OPEN */
const [retryConnection, setRetryConnection] = useState(true); /* set when connection should be re-established */
const [ name, setName ] = useState("");
const [ error, setError ] = useState(undefined);
const [ warning, setWarning ] = useState(undefined);
const [loaded, setLoaded] = useState(false);
const [state, setState] = useState(undefined);
const [color, setColor] = useState(undefined);
const [priv, setPriv] = useState(undefined);
const [buildActive, setBuildActive] = useState(false);
const [tradeActive, setTradeActive] = useState(false);
const [cardActive, setCardActive] = useState(undefined);
const [houseRulesActive, setHouseRulesActive] = useState(undefined);
const [winnerDismissed, setWinnerDismissed] = useState(undefined);
const [global, setGlobal] = useState({});
const [count, setCount] = useState(0);
const fields = [ 'id', 'state', 'color', 'name', 'private' ];
const onWsOpen = (event) => {
console.log(`ws: open`);
setError("");
/* We do not set the socket as connected until the 'open' message
* comes through */
setConnection(ws);
/* 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 */
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(`App - error`, data.error);
setError(data.error);
break;
case 'warning':
console.warn(`App - warning`, data.warning);
setWarning(data.warning);
setTimeout(() => {
console.log(`todo: stack warnings in a window and have them disappear one at a time.`);
console.log(`app - clearing warning`);
setWarning("");
}, 3000);
break;
case 'game-update':
if (!loaded) {
setLoaded(true);
}
console.log(`ws: message - ${data.type}`, data.update);
if ('private' in data.update && !equal(priv, data.update.private)) {
const priv = data.update.private;
if (priv.name !== name) {
console.log(`App - setting name (via private): ${priv.name}`);
setName(priv.name);
}
if (priv.color !== color) {
console.log(`App - setting color (via private): ${priv.color}`);
setColor(priv.color);
}
setPriv(priv);
}
if ('name' in data.update) {
if (data.update.name) {
console.log(`App - setting name: ${data.update.name}`);
setName(data.update.name);
} else {
setWarning("");
setError("");
setPriv(undefined);
}
}
if ('id' in data.update && data.update.id !== gameId) {
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}`);
if (data.update.state !== 'winner' && winnerDismissed) {
setWinnerDismissed(false);
}
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;
}
};
const sendUpdate = (update) => {
ws.send(JSON.stringify(update));
};
const cbResetConnection = useCallback(() => {
let timer = 0;
function reset() {
timer = 0;
setRetryConnection(true);
};
return _ => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(reset, 5000);
};
}, [setRetryConnection]);
const resetConnection = cbResetConnection();
if (global.ws !== connection
|| global.name !== name
|| global.gameId !== gameId) {
console.log(`board - (app) - setting global`, global, {
ws: connection,
name,
gameId
});
setGlobal({
ws: connection,
name,
gameId
});
}
const onWsError = (event) => {
console.error(`ws: error`, event);
const error = `Connection to Ketr Ketran game server failed! ` +
`Connection attempt will be retried every 5 seconds.`;
setError(error);
setGlobal(Object.assign({}, global, { ws: undefined }));
setWs(undefined); /* clear the socket */
setConnection(undefined); /* clear the connection */
resetConnection();
};
const onWsClose = (event) => {
const error = `Connection to Ketr Ketran game was lost. ` +
`Attempting to reconnect...`;
console.warn(`ws: close`);
setError(error);
setGlobal(Object.assign({}, global, { ws: undefined }));
setWs(undefined); /* clear the socket */
setConnection(undefined); /* clear the connection */
resetConnection();
};
/* callback refs are used to provide correct state reference
* in the callback handlers, while also preventing rebinding
* of event handlers on every render */
const refWsOpen = useRef(onWsOpen);
useEffect(() => { refWsOpen.current = onWsOpen; });
const refWsMessage = useRef(onWsMessage);
useEffect(() => { refWsMessage.current = onWsMessage; });
const refWsClose = useRef(onWsClose);
useEffect(() => { refWsClose.current = onWsClose; });
const refWsError = useRef(onWsError);
useEffect(() => { refWsError.current = onWsError; });
/* This effect is responsible for triggering a new game load if a
* game id is not provided in the URL. If the game is provided
* in the URL, the backend will create a new game if necessary
* during the WebSocket connection sequence.
*
* This should be the only HTTP request made from the game.
*/
useEffect(() => {
if (gameId) {
console.log(`Game in use ${gameId}`)
return;
}
console.log(`Requesting new game.`);
window.fetch(`${base}/api/v1/games/`, {
method: 'POST',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
}).then((res) => {
if (res.status >= 400) {
const error = `Unable to connect to Ketr Ketran game server! ` +
`Try refreshing your browser in a few seconds.`;
console.error(error);
setError(error);
throw new Error(error);
}
return res.json();
}).then((update) => {
if (update.id !== gameId) {
console.log(`Game available: ${update.id}`);
history.push(`${gamesPath}/${update.id}`);
setGameId(update.id);
}
}).catch((error) => {
console.error(error);
});
}, [ gameId, setGameId ]);
/* Once a game id is known, create the sole WebSocket connection
* to the backend. This WebSocket is then shared with any component
* that performs game state updates. Those components should
* bind to the 'message:game-update' WebSocket event and parse
* their update information from those messages
*/
useEffect(() => {
if (!gameId) {
return;
}
const unbind = () => {
console.log(`table - unbind`);
}
console.log(`table - bind`);
if (!ws && !connection && retryConnection) {
let loc = window.location, new_uri;
if (loc.protocol === "https:") {
new_uri = "wss";
} else {
new_uri = "ws";
}
new_uri = `${new_uri}://${loc.host}${base}/api/v1/games/ws/${gameId}?${count}`;
console.log(`Attempting WebSocket connection to ${new_uri}`);
setWs(new WebSocket(new_uri));
setConnection(undefined);
setRetryConnection(false);
setCount(count + 1);
return unbind;
}
if (!ws) {
return unbind;
}
const cbOpen = e => refWsOpen.current(e);
const cbMessage = e => refWsMessage.current(e);
const cbClose = e => refWsClose.current(e);
const cbError = e => refWsError.current(e);
ws.addEventListener('open', cbOpen);
ws.addEventListener('close', cbClose);
ws.addEventListener('error', cbError);
ws.addEventListener('message', cbMessage);
return () => {
unbind();
ws.removeEventListener('open', cbOpen);
ws.removeEventListener('close', cbClose);
ws.removeEventListener('error', cbError);
ws.removeEventListener('message', cbMessage);
}
}, [ ws, setWs, connection, setConnection,
retryConnection, setRetryConnection, gameId,
refWsOpen, refWsMessage, refWsClose, refWsError, count, setCount
]);
console.log(`board - (app) - Render with ws: ${ws ? '!' : ''}NULL, connection: ${connection ? '!' : ''}NULL`);
return <GlobalContext.Provider value={global}>
{ /* <PingPong/> */ }
<div className="Table">
<Activities/>
<div className="Game">
<div className="Dialogs">
{ error && <div className="Dialog ErrorDialog">
<Paper className="Error">
<div>{ error }</div>
<Button onClick={() => { setError("")}}>dismiss</Button>
</Paper>
</div> }
{ priv && priv.turnNotice && <div className="Dialog TurnNoticeDialog">
<Paper className="TurnNotice">
<div>{ priv.turnNotice }</div>
<Button onClick={() => { sendUpdate({type: 'turn-notice'}) }}>dismiss</Button>
</Paper>
</div> }
{ warning && <div className="Dialog WarningDialog">
<Paper className="Warning">
<div>{ warning }</div>
<Button onClick={() => { setWarning("")}}>dismiss</Button>
</Paper>
</div> }
{ state === 'normal' && <SelectPlayer/> }
{ color && state === 'game-order' && <GameOrder/> }
{ !winnerDismissed && <Winner {...{winnerDismissed, setWinnerDismissed}}/> }
{ houseRulesActive && <HouseRules {...{houseRulesActive, setHouseRulesActive}}/> }
<ViewCard {...{cardActive, setCardActive }}/>
<ChooseCard/>
</div>
<Board/>
<PlayersStatus/>
<PlayersStatus active={true}/>
<Hand {...{buildActive, setBuildActive, setCardActive}}/>
</div>
<div className="Sidebar">
{ name !== "" && <PlayerList/> }
<Trade {...{tradeActive, setTradeActive}}/>
{ name !== "" && <Chat/> }
{ /* name !== "" && <VideoFeeds/> */ }
{ loaded && <Actions {
...{buildActive, setBuildActive,
tradeActive, setTradeActive,
houseRulesActive, setHouseRulesActive
}}/> }
</div>
</div>
</GlobalContext.Provider>;
};
const App = () => {
const [playerId, setPlayerId] = useState(undefined);
const [error, setError] = useState(undefined);
useEffect(() => {
if (playerId) {
return;
}
window.fetch(`${base}/api/v1/games/`, {
method: 'GET',
cache: 'no-cache',
credentials: 'same-origin', /* include cookies */
headers: {
'Content-Type': 'application/json'
},
}).then((res) => {
if (res.status >= 400) {
const error = `Unable to connect to Ketr Ketran game server! ` +
`Try refreshing your browser in a few seconds.`;
console.error(error);
setError(error);
}
console.log(res.headers);
return res.json();
}).then((data) => {
setPlayerId(data.player);
}).catch((error) => {
});
}, [playerId, setPlayerId]);
if (!playerId) {
return <>{ error }</>;
}
return (
<Router>
<Routes>
<Route exact element={<Table/>} path={`${base}/:gameId`}/>
<Route exact element={<Table/>} path={`${base}`}/>
</Routes>
</Router>
);
}
export default App;