import React, { useState } from "react"; import "./Table.css"; import history from "./history.js"; import Paper from '@material-ui/core/Paper'; import Button from '@material-ui/core/Button'; import TextField from '@material-ui/core/TextField'; import List from '@material-ui/core/List'; import Board from './Board.js'; import Trade from './Trade.js'; import PlayerColor from './PlayerColor.js'; import Dice from './Dice.js'; import Resource from './Resource.js'; import ViewCard from './ViewCard.js'; import Winner from './Winner.js'; import ChooseCard from './ChooseCard.js'; import Chat from './Chat.js'; import { CircularProgress } from "@material-ui/core"; import 'moment-timezone'; import Activities from './Activities.js'; import Placard from './Placard.js'; import PlayersStatus from './PlayersStatus.js'; import { MediaAgent, MediaControl, MediaContext } from './MediaControl.js'; import { base, assetsPath, getPlayerName, gamesPath } from './Common.js'; /* Start of withRouter polyfill */ // https://reactrouter.com/docs/en/v6/faq#what-happened-to-withrouter-i-need-it import { useLocation, useNavigate, useParams } from "react-router-dom"; function withRouter(Component) { function ComponentWithRouterProp(props) { let location = useLocation(); let navigate = useNavigate(); let params = useParams(); return ( ); } return ComponentWithRouterProp; } /* end of withRouter polyfill */ const StartButton = ({ table, game }) => { const startClick = (event) => { table.setGameState("game-order"); }; return ( ); }; /* This needs to take in a mechanism to declare the * player's active item in the game */ const Players = ({ table, game }) => { const toggleSelected = (key) => { table.setSelected(game.color === key ? "" : key); } const players = []; if (!game.id) { return (<>); } for (let color in game.players) { const item = game.players[color], inLobby = game.state === 'lobby'; if (!inLobby && item.status === 'Not active') { continue; } let name = getPlayerName(game.sessions, color), selectable = game.state === 'lobby' && (item.status === 'Not active' || game.color === color); players.push((
{ inLobby && selectable && toggleSelected(color) }} key={`player-${color}`}> {name ? name : 'Available' } { name && } { !name &&
}
)); } return ( { players } ); } console.log("TODO: Convert this to a function component!!!!"); class Table extends React.Component { constructor(props) { super(props); this.state = { message: "", error: "", signature: "", buildActive: false, cardActive: undefined, loading: 0, noNetwork: false, ws: undefined, peers: {} }; this.componentDidMount = this.componentDidMount.bind(this); this.throwDice = this.throwDice.bind(this); this.rollDice = this.rollDice.bind(this); this.setGameState = this.setGameState.bind(this); this.shuffleTable = this.shuffleTable.bind(this); this.startTrading = this.startTrading.bind(this); this.offerTrade = this.offerTrade.bind(this); this.acceptTrade = this.acceptTrade.bind(this); this.rejectTrade = this.rejectTrade.bind(this); this.cancelTrading = this.cancelTrading.bind(this); this.discard = this.discard.bind(this); this.passTurn = this.passTurn.bind(this); this.updateGame = this.updateGame.bind(this); this.setPlayerName = this.setPlayerName.bind(this); this.setSelected = this.setSelected.bind(this); this.updateMessage = this.updateMessage.bind(this); this.sendAction = this.sendAction.bind(this); this.buildClicked = this.buildClicked.bind(this); this.closeCard = this.closeCard.bind(this); this.playCard = this.playCard.bind(this); this.selectResources = this.selectResources.bind(this); this.buildItem = this.buildItem.bind(this); this.loadTimer = null; this.peers = {}; this.id = (props.router && props.router.params.id) ? props.router.params.id : 0; this.setPeers = this.setPeers.bind(this); } setPeers(update) { for (let key in this.peers) { if (!(key in update)) { delete this.peers[key]; } } this.setState({ peers: Object.assign({}, this.peers, update)}); } closeCard() { this.setState({cardActive: undefined}); } sendAction(action, value, extra) { if (this.loadTimer) { window.clearTimeout(this.loadTimer); this.loadTimer = null; } if (value === undefined || value === null) { value = ''; } this.setState({ loading: this.state.loading + 1 }); return window.fetch(`${base}/api/v1/games/${this.state.id}/${action}/${value}`, { method: "PUT", cache: 'no-cache', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: extra ? JSON.stringify(extra) : undefined }).then((res) => { if (res.status >= 400) { throw new Error(`Unable to perform ${action}!`); } return res.json(); }).then((game) => { const error = (game.status !== 'success') ? game.status : undefined; this.updateGame(game); this.updateMessage(); this.setError(error); }).catch((error) => { console.error(error); this.setError(error.message); }).then(() => { this.setState({ loading: this.state.loading - 1 }); }); } setSelected(key) { return this.sendAction('player-selected', key); } sendChat(message) { return this.sendAction('chat', undefined, {message: message}); } selectResources(cards) { return this.sendAction('select-resources', undefined, cards); } playCard(card) { this.setState({ cardActive: undefined }); return this.sendAction('play-card', undefined, card); } setPlayerName(name) { return this.sendAction('player-name', name) .then(() => { this.setState({ pickName: false }); }); } shuffleTable() { return this.sendAction('shuffle') .then(() => { this.setError("Table shuffled!"); }); } startTrading() { return this.sendAction('trade'); } cancelTrading() { return this.sendAction('trade', 'cancel'); } offerTrade(trade) { return this.sendAction('trade', 'offer', trade); } acceptTrade(trade) { return this.sendAction('trade', 'accept', trade); } cancelTrade(trade) { return this.sendAction('trade', 'cancel', trade); } rejectTrade(trade) { return this.sendAction('trade', 'reject', trade); } discard(resources) { return this.sendAction('discard', undefined, resources); } passTurn() { return this.sendAction('pass'); }; rollDice() { return this.sendAction('roll'); } setError(error) { if (!error) { return; } if (this.errorTimeout) { clearTimeout(this.errorTimeout); } setTimeout(() => { this.setState({error: undefined}) }, 3000); if (this.state.error !== error) { this.setState({ error }); } } setGameState(state) { if (this.loadTimer) { window.clearTimeout(this.loadTimer); this.loadTimer = null; } this.setState({ loading: this.state.loading + 1 }); return window.fetch(`${base}/api/v1/games/${this.state.id}/state/${state}`, { method: "PUT", cache: 'no-cache', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' } }).then((res) => { if (res.status >= 400) { console.log(res); throw new Error(`Unable to set state to ${state}`); } return res.json(); }).then((game) => { console.log (`Game state set to ${game.state}!`); this.updateGame(game); this.updateMessage(); }).catch((error) => { console.error(error); this.setError(error.message); }).then(() => { this.setState({ loading: this.state.loading - 1 }); return this.state.state; }); } buildClicked(event) { console.log("Build clicked"); this.setState({ buildActive: this.state.buildActive ? false : true }); }; placeRobber(robber) { return this.sendAction('place-robber', robber); }; buyDevelopment() { return this.sendAction('buy-development'); } buySettlement() { return this.sendAction('buy-settlement'); } placeSettlement(settlement) { return this.sendAction('place-settlement', settlement); } buyCity() { return this.sendAction('buy-city'); } placeCity(city) { return this.sendAction('place-city', city); } buyRoad() { return this.sendAction('buy-road'); } placeRoad(road) { return this.sendAction('place-road', road); } stealResource(color) { return this.sendAction('steal-resource', color); } throwDice() { return this.rollDice(); } updateGame(game) { if (this.state.signature !== game.signature) { this.setState( { signature: game.signature }); } console.log("Update Game", game); /* Only update fields that are changing */ for (let key in game) { if (game[key] === this.state[key]) { delete game[key]; } } console.log(`Updating: `, { ...game }); this.setState( { ...game } ); } updateMessage() { const player = (this.state.id && this.state.color) ? this.state.players[this.state.color] : undefined, name = this.state ? this.state.name : ""; let message = <>; if (this.state.pickName || !name) { message = <>{message}Enter the name you would like to be known by, then press ENTER or select  SET.; } else { switch (this.state.state) { case 'lobby': message = <>{message}You are in the lobby as {name}.; if (!this.state.color) { message = <>{message}You select one of the Available colors below.; } else { message = <>{message}You have selected .; } message = <>{message}You can chat with other players below.; if (this.state.active < 2) { message = <>{message}Once there are two or more players, you can select .; } else { message = <>{message}There are enough players to start the game. Select when ready.; } break; case 'game-order': if (!player) { message = <>{message}You are an observer in this game as  {name}.; message = <>{message}You can chat with other players below as {this.state.name}, but cannot play unless players go back to the Lobby.; } else { if (!player.order) { message = <>{message}You need to roll for game order. Click Roll Dice below.; } else { message = <>{message}You rolled for game order. Waiting for all players to roll.; } } break; case 'initial-placement': message = <>{message}It is time for all the players to place their initial two settlements, with one road connected to each settlement.; break; case 'active': if (!player) { message = <>{message}This game is no longer in the lobby.
TODO: Override game state to allow Lobby mode while in-game; } break; case null: case undefined: case '': message = <>{message}The game is in a wonky state. Sorry :(; break; case 'normal': if (this.state.turn) { if (this.state.turn.roll === 7) { message = <>{message}Robber was rolled!; let move = true; for (let color in this.state.players) { let name = ''; for (let i = 0; i < this.state.sessions.length; i++) { if (this.state.sessions[i].color === color) { name = this.state.sessions[i].name; } } const discard = this.state.players[color].mustDiscard; if (discard) { move = false; message = <>{message} {name} needs to discard {discard} resources.; } } if (move && (this.state.turn && !this.state.turn.placedRobber)) { message = <>{message} {this.state.turn.name} needs to move the robber. } } else { message = <>It is {this.state.turn.name}'s turn.; } } break; default: message = <>{message}Game state is: {this.state.state}; break; } } this.setState({ message: message }); } resetKeepAlive(isDead) { if (isDead) { console.log(`Short circuiting keep-alive`); if (this.ws) { this.ws.close(); delete this.ws; } } else { // console.log(`${this.game.name} Resetting keep-alive. Last ping: ${(Date.now() - this.lastPing) / 1000}`); } if (this.keepAlive) { clearTimeout(this.keepAlive); this.keepAlive = 0; } else { console.log(`No keep-alive active`); } this.keepAlive = setTimeout(() => { console.log(`${this.state.name} No ping after 10 seconds. Last ping: ${(Date.now() - this.lastPing) / 1000}`); this.setState({ noNetwork: true }); if (this.ws) { this.ws.close(); delete this.ws; } this.connectWebSocket(); }, isDead ? 3000 : 10000); if (this.state.noNetwork !== false && !isDead) { this.setState({ noNetwork: false }); } else if (this.state.noNetwork !== true && isDead) { this.setState({ noNetwork: true }); } } connectWebSocket() { if (!this.state.id) { console.log(`Cannot initiate websocket connection while no game is set.`); this.resetKeepAlive(true); return; } if (this.ws) { return; } 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/${this.state.id}/`; console.log(`Attempting WebSocket connection to ${new_uri}`); this.ws = new WebSocket(new_uri); this.setState({ ws: this.ws }); this.lastPing = this.state.timestamp; this.ws.addEventListener('message', (event) => { this.resetKeepAlive(); let data; try { data = JSON.parse(event.data); } catch (error) { this.setError(error); return; } let update; switch (data.type) { case 'game-update': console.log(`Game update received`); update = data.update; const error = (update.status !== 'success') ? update.status : undefined; this.updateGame(update); this.updateMessage(); this.setError(error); break; case 'ping': this.lastPing = data.ping; this.ws.send(JSON.stringify({ type: 'pong', timestamp: data.ping })); break; default: break; } }); this.ws.addEventListener('error', (event) => { this.setState({ error: event.message }); console.error(`${this.state.name} WebSocket error: ${(Date.now() - this.state.lastPing) / 1000}`); this.resetKeepAlive(true); }); this.ws.addEventListener('close', (event) => { console.log(`${this.state.name} WebSocket close: ${(Date.now() - this.state.lastPing) / 1000}`); this.setState({ error: event.message }); this.resetKeepAlive(true); }); this.ws.addEventListener('open', (event) => { console.log(`${this.state.name} WebSocket open: Sending game-update request: ${(Date.now() - this.lastPing) / 1000}`); this.ws.send(JSON.stringify({ type: 'game-update' })); this.resetKeepAlive(); }); } componentDidMount() { this.start = new Date(); console.log(`Mounted: ${base}`); const params = {}; if (this.id) { console.log(`Loading game: ${this.id}`); params.url = `${base}/api/v1/games/${this.id}`; params.method = "GET" } else { console.log("Requesting new game."); params.url = `${base}/api/v1/games/`; params.method = "POST"; } this.setState({ loading: this.state.loading + 1 }); window.fetch(params.url, { method: params.method, cache: 'no-cache', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, // body: JSON.stringify(data) // body data type must match "Content-Type" header }).then((res) => { if (res.status < 400) { return res; } let error; if (!this.id) { error = `Unable to create new game.`; throw new Error(error); } error = `Unable to find game ${this.id}. Starting new game.` console.log(error); this.setError(error); params.url = `${base}/api/v1/games/${this.id}`; params.method = "POST"; return window.fetch(params.url, { method: params.method, cache: 'no-cache', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' } }); }).then((res) => { return res.json(); }).then((game) => { if (!this.id) { history.push(`${gamesPath}/${game.id}`); } this.id = game.id; this.updateGame(game); /* Connect to the WebSocket (after the game is setup) */ this.connectWebSocket(); this.updateMessage(); this.setState({ error: "" }); }).catch((error) => { console.error(error); this.setError(error.message); }).then(() => { this.setState({ loading: this.state.loading - 1 }); }); } buildItem(item) { return this.sendAction(`buy-${item}`); } componentWillUnmount() { if (this.ws) { this.ws.close(); this.ws = null; } if (this.loadTimer) { clearTimeout(this.loadTimer); this.loadTimer = 0; } if (this.keepAlive) { clearTimeout(this.keepAlive); this.keepAlive = 0; } if (this.updateSizeTimer) { clearTimeout(this.updateSizeTimer); this.updateSizeTimer = 0; } if (this.errorTimeout) { clearTimeout(this.errorTimeout); this.errorTimeout = 0; } } cardClicked(card) { this.setState({cardActive: card }); } render() { const game = this.state, player = game ? game.player : undefined, isTurn = (game && game.turn && game.turn.color === game.color) ? true : false, showMessage = (game && (game.state === 'lobby' || !game.name)); let color; switch (game ? game.color : undefined) { case "O": color = "orange"; break; case "R": color = "red"; break; case "B": color = "blue"; break; default: case "W": color = "white"; break; } let development; if (player) { let stacks = {}; game.player.development.forEach(card => (card.type in stacks) ? stacks[card.type].push(card) : stacks[card.type] = [card]); development = []; for (let type in stacks) { const cards = stacks[type] .sort((A, B) => { if (A.played) { return -1; } if (B.played) { return +1; } return 0; }).map(card => this.cardClicked(card)} card={card} table={this} key={`${type}-${card.card}`} type={`${type}-${card.card}`}/>); development.push(
{ cards }
); } } else { development = <>/; } if (!this.state.id) { return <>; } return (<>
{ this.state.loading > 0 && } { this.state.noNetwork &&
}
{ player !== undefined && <>
{ development }
{ game.longestRoad && game.longestRoad === game.color && } { game.largestArmy && game.largestArmy === game.color && }
} { player === undefined &&
}
{ game &&
{ showMessage && { this.state.message } } {(this.state.pickName || !game.name) && } {(!this.state.pickName && game.name) && <> }
} { game && game.state === 'winner' && } { this.state.cardActive && } { game && game.state === 'game-order' && } { game && game.state === 'normal' && game.turn.actions && game.turn.actions.indexOf('trade') !== -1 && } { game && isTurn && game.turn.actions && game.turn.actions.indexOf('select-resources') !== -1 && } { game && game.state === 'normal' && game.turn && isTurn && game.turn.actions && game.turn.actions.indexOf('steal-resource') !== -1 && } { this.state.error && this.setState({ error: undefined })} className="Error">
{this.state.error}
}
); } } export default withRouter(props => );