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 { MediaAgent } from "./MediaControl.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 history from "./history.js"; import "./App.css"; const Table = () => { const params = useParams(); const [ gameId, setGameId ] = useState(params.gameId ? params.gameId : undefined); const [ ws, setWs ] = useState(); const [ name, setName ] = useState(""); const [ error, setError ] = useState(undefined); const [ warning, setWarning ] = useState(undefined); const [ peers, setPeers ] = useState({}); const [loaded, setLoaded] = useState(false); const [connecting, setConnecting] = useState(undefined); const [state, setState] = useState(undefined); const [color, setColor] = useState(undefined); const [players, setPlayers] = useState(undefined); const [player, setPlayer] = useState(undefined); const [buildActive, setBuildActive] = useState(false); const [cardActive, setCardActive] = useState(undefined); const fields = [ 'id', 'state', 'color', 'name' ]; useEffect(() => { console.log(`app - media-agent - peers`, peers); }, [peers]); const onWsOpen = (event) => { console.log(`ws: open`); setError(""); /* We do not set the socket as connected until the 'open' message * comes through */ 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 */ 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 ('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 ('players' in data.update) { 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) { console.log(`App - setting name: ${data.update.name}`); setName(data.update.name); } 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}`); setState(data.update.state); } if ('color' in data.update && data.update.color !== color) { console.log(`App - setting color: ${color}`); setColor(data.update.color); if (players && players[data.update.color] !== player) { setPlayer(players[data.update.color]); } } break; default: break; } }; const cbResetConnection = useCallback(() => { let timer = 0; function reset() { timer = 0; setConnecting(undefined); }; return _ => { if (timer) { clearTimeout(timer); } timer = setTimeout(reset, 5000); }; }, [setConnecting]); const resetConnection = cbResetConnection(); 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); setWs(undefined); resetConnection(); }; const onWsClose = (event) => { const error = `Connection to Ketr Ketran game was lost. ` + `Attempting to reconnect...`; console.warn(`ws: close`); setError(error); setWs(undefined); 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 error; } return res.json(); }).then((update) => { if (update.id !== gameId) { console.log(`New game started: ${update.id}`); history.push(`${gamesPath}/${update.id}`); setGameId(update.id); } }); }, [ 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 && !connecting) { 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}`; console.log(`Attempting WebSocket connection to ${new_uri}`); setWs(new WebSocket(new_uri)); setConnecting(undefined); 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); } }, [ setWs, connecting, setConnecting, gameId, ws, refWsOpen, refWsMessage, refWsClose, refWsError ]); return { /* */ } { error && { error } { setError("")}}>dismiss } { warning && { warning } } { state === 'normal' && } { color && state === 'game-order' && } { name !== "" && } { name !== "" && } { loaded && } ; }; 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 ( } path={`${base}/:gameId`}/> } path={`${base}`}/> ); } export default App;