1
0
James Ketrenos 42d076f216 Game seems to be working
Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
2022-03-14 00:07:50 -07:00

373 lines
11 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 { 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 <GlobalContext.Provider value={{ ws: connecting, name, gameId, peers, setPeers }}>
<MediaAgent/>
{ /* <PingPong/> */ }
<div className="Table">
<Activities/>
<Trade/>
<div className="Game">
<div className="Dialogs">
{ error && <div className="ErrorDialog">
<Paper className="Error">
<div>{ error }</div>
<Button onClick={() => { setError("")}}>dismiss</Button>
</Paper>
</div> }
{ warning && <div className="WarningDialog">
<Paper className="Warning">{ warning }</Paper>
</div> }
{ state === 'normal' && <SelectPlayer/> }
{ color && state === 'game-order' && <GameOrder/> }
<Winner/>
<ViewCard {...{cardActive, setCardActive }}/>
<ChooseCard/>
</div>
<Board/>
<PlayersStatus/>
<PlayersStatus active={true}/>
<Hand {...{buildActive, setBuildActive, setCardActive}}/>
</div>
<div className="Sidebar">
{ name !== "" && <PlayerList/> }
{ name !== "" && <Chat/> }
{ loaded && <Actions {...{buildActive, setBuildActive}}/> }
</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;