import React, { useState, useEffect } 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 ListItem from '@material-ui/core/ListItem'; import ListItemText from '@material-ui/core/ListItemText'; import Moment from 'react-moment'; import Board from './Board.js'; import Trade from './Trade.js'; import { assetsPath, base, getPlayerName, gamesPath } from './Common.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 { CircularProgress } from "@material-ui/core"; import 'moment-timezone'; /* 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 Placard = ({table, type, active}) => { const dismissClicked = (event) => { table.setState({ buildActive: false }); } const buildClicked = (event) => { if (!type.match(/^l.*/)) { if (!table.state.buildActive) { table.setState({ buildActive: true }); } } }; const roadClicked = (event) => { table.buyRoad(); table.setState({ buildActive: false }); }; const settlementClicked = (event) => { table.buySettlement(); table.setState({ buildActive: false }); }; const cityClicked = (event) => { table.buyCity(); table.setState({ buildActive: false }); }; const developmentClicked = (event) => { table.buyDevelopment(); table.setState({ buildActive: false }); }; let buttons; switch (active ? type : undefined) { case 'orange': case 'red': case 'white': case 'blue': buttons = <>
; break; default: buttons = <>; break; } return (
{buttons}
); }; const Development = ({table, type, card, onClick}) => { return (
); }; const Chat = ({ table }) => { const [lastTop, setLastTop] = useState(0), [autoScroll, setAutoscroll] = useState(true), [latest, setLatest] = useState(''), [scrollTime, setScrollTime] = useState(0); const chatInput = (event) => { }; const chatKeyPress = (event) => { if (event.key === "Enter") { if (!autoScroll) { setAutoscroll(true); } table.sendChat(event.target.value); event.target.value = ""; } }; const chatScroll = (event) => { const chatList = event.target, fromBottom = Math.round(Math.abs((chatList.scrollHeight - chatList.offsetHeight) - chatList.scrollTop)); /* If scroll is within 20 pixels of the bottom, turn on auto-scroll */ const shouldAutoscroll = (fromBottom < 20); if (shouldAutoscroll !== autoScroll) { setAutoscroll(shouldAutoscroll); } /* If the list should not auto scroll, then cache the current * top of the list and record when we did this so we honor * the auto-scroll for at least 500ms */ if (!shouldAutoscroll) { const target = Math.round(chatList.scrollTop); if (target !== lastTop) { setLastTop(target); setScrollTime(Date.now()); } } }; useEffect(() => { const chatList = document.getElementById("ChatList"), currentTop = Math.round(chatList.scrollTop); if (autoScroll) { /* Auto-scroll to the bottom of the chat window */ const target = Math.round(chatList.scrollHeight - chatList.offsetHeight); if (currentTop !== target) { chatList.scrollTop = target; } return; } /* Maintain current position in scrolled view if the user hasn't * been scrolling in the past 0.5s */ if ((Date.now() - scrollTime) > 500 && currentTop !== lastTop) { chatList.scrollTop = lastTop; } }); //const timeDelta = game.timestamp - Date.now(); if (!table.game) { console.log("Why no game?"); } const messages = table.game && table.game.chat.map((item, index) => { let message; /* If the date is in the future, set it to now */ const dice = item.message.match(/^(.*rolled )([1-6])(, ([1-6]))?(.*)$/); if (dice) { if (dice[4]) { message = <>{dice[1]}, {dice[5]}; } else { message = <>{dice[1]}{dice[5]}; } } else { message = item.message; } return ( { item.color && } Date.now() ? Date.now() : item.date} interval={1000}/>} /> ); }); if (table.game && table.game.chat && table.game.chat.length && table.game.chat[table.game.chat.length - 1].date !== latest) { setLatest(table.game.chat[table.game.chat.length - 1].date); setAutoscroll(true); } const name = table.game ? table.game.name : "Why no game?"; const elapsed = table.game ? (table.game.timestamp - table.game.startTime) : undefined; return ( { messages } } variant="outlined"/> ); } const StartButton = ({ table }) => { const startClick = (event) => { table.setGameState("game-order").then((state) => { table.game.state = state; }); }; return ( ); }; const WaitingForPlayer = ({table}) => { return (
{ table.game && table.game.turn &&
Waiting for {table.game.turn.name} to complete their turn.
}
); } const GameOrder = ({table}) => { const rollClick = (event) => { table.throwDice(); } if (!table.game) { return (<>); } let players = [], hasRolled = true; for (let color in table.game.players) { const item = table.game.players[color], name = getPlayerName(table.game.sessions, color); if (color === table.game.color) { hasRolled = item.orderRoll !== 0; } if (name) { if (!item.orderRoll) { item.orderRoll = 0; } players.push({ name: name, color: color, ...item }); } } players.sort((A, B) => { if (A.order === B.order) { if (A.orderRoll === B.orderRoll) { return A.name.localeCompare(B.name); } return B.orderRoll - A.orderRoll; } return B.order - A.order; }); players = players.map(item =>
{item.name}
{ item.orderRoll !== 0 && <>rolled . {item.orderStatus} } { item.orderRoll === 0 && <>has not rolled yet. {item.orderStatus}}
); return (
{ table.game &&
Game Order
{ players }
}
); }; const SelectPlayer = ({table, players}) => { const playerClick = (event) => { table.stealResource(event.currentTarget.getAttribute('data-color')); } if (!table.game) { return (<>); } let list = players.map(color => { let item = { color: color, name: getPlayerName(table.game.sessions, color) }; return
{item.name}
; }); return (
{ table.game &&
Select Player to Steal From
{ list }
}
); }; const Action = ({ table }) => { const buildClicked = (event) => { table.buildClicked(event); }; const discardClick = (event) => { const nodes = document.querySelectorAll('.Hand .Resource.Selected'), discarding = { wheat: 0, brick: 0, sheep: 0, stone: 0, wood: 0 }; for (let i = 0; i < nodes.length; i++) { discarding[nodes[i].getAttribute("data-type")]++; nodes[i].classList.remove('Selected'); } return table.discard(discarding); } const newTableClick = (event) => { return table.shuffleTable(); }; const tradeClick = (event) => { table.startTrading(); } const rollClick = (event) => { table.throwDice(); } const passClick = (event) => { return table.passTurn(); } const quitClick = (event) => { table.setSelected(""); } if (!table.game) { console.log("Why no game?"); return (); } const game = table.game, inLobby = game.state === 'lobby', player = game ? game.player : undefined, hasRolled = (game && game.turn && game.turn.roll) ? true : false, isTurn = (game && game.turn && game.turn.color === game.color) ? true : false, robberActions = (game && game.turn && game.turn.robberInAction), haveResources = player ? player.haveResources : false; return ( { inLobby && <> } { !inLobby && <> { game.turn.roll === 7 && player && player.mustDiscard > 0 && } } { /* inLobby && */ } ); } const PlayerName = ({table}) => { const [name, setName] = useState((table && table.game && table.game.name) ? table.game.name : ""); const nameChange = (event) => { setName(event.target.value); } const sendName = () => { if (name !== table.game.name) { table.setPlayerName(name); } else { table.setError(""); table.setState({ pickName: false }); } } const nameKeyPress = (event) => { if (event.key === "Enter") { sendName(); } } return ( ); }; /* This needs to take in a mechanism to declare the * player's active item in the game */ const Players = ({ table }) => { const toggleSelected = (key) => { table.setSelected(table.game.color === key ? "" : key); } const players = []; if (!table.game) { return (<>); } for (let color in table.game.players) { const item = table.game.players[color], inLobby = table.game.state === 'lobby'; if (!inLobby && item.status === 'Not active') { continue; } const name = getPlayerName(table.game.sessions, color), selectable = table.game.state === 'lobby' && (item.status === 'Not active' || table.game.color === color); let toggleText; if (name) { toggleText = `${name} has ${item.points} VP`; if (item.unplayed) { toggleText += ` and ${item.unplayed} unplayed DCs`; } } else { toggleText = "Available"; } players.push((
{ inLobby && selectable && toggleSelected(color) }} key={`player-${color}`}> { item.status + ' ' } { item.status !== 'Not active' && Date.now() ? Date.now() : item.lastActive}/>} )} />
)); } return ( { players } ); } console.log("TODO: Convert this to a function component!!!!"); class Table extends React.Component { constructor(props) { super(props); this.state = { total: 0, wood: 0, sheep: 0, brick: 0, stone: 0, wheat: 0, game: null, message: "", error: "", signature: "", buildActive: false, cardActive: undefined, loading: 0, noNetwork: false }; 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.selectResource = this.selectResource.bind(this); this.mouse = { x: 0, y: 0 }; this.radius = 0.317; this.loadTimer = null; this.game = null; this.pips = []; this.tiles = []; this.borders = []; this.tabletop = null; this.closest = { info: {}, tile: null, road: null, tradeToken: null, settlement: null }; this.id = (props.router && props.router.params.id) ? props.router.params.id : 0; } 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.game.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}); } selectResource(card) { return this.sendAction('select-resource', card); } 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); } 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.game.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.game.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); this.setState( { game }); this.game = game; } updateMessage() { const player = (this.game && this.game.color) ? this.game.players[this.game.color] : undefined, name = this.game ? this.game.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.game && this.game.state) { case 'lobby': message = <>{message}You are in the lobby as {name}.; if (!this.game.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.game.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.game.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.game && this.game.turn) { if (this.game.turn.roll === 7) { message = <>{message}Robber was rolled!; let move = true; for (let color in this.game.players) { let name = ''; for (let i = 0; i < this.game.sessions.length; i++) { if (this.game.sessions[i].color === color) { name = this.game.sessions[i].name; } } const discard = this.game.players[color].mustDiscard; if (discard) { move = false; message = <>{message} {name} needs to discard {discard} resources.; } } if (move && (this.game.turn && !this.game.turn.placedRobber)) { message = <>{message} {this.game.turn.name} needs to move the robber. } } else { message = <>It is {this.game.turn.name}'s turn.; } } break; default: message = <>{message}Game state is: {this.game.state}; break; } } this.setState({ message: message }); } resetKeepAlive() { if (this.keepAlive) { clearTimeout(this.keepAlive); } this.keepAlive = setTimeout(() => { console.error(`No server ping for 5 seconds!`); this.setState({ noNetwork: true }); }, 5000); } connectWebSocket() { 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.id}`; console.log(`Attempting WebSocket connection to ${new_uri}`); this.ws = new WebSocket(new_uri); this.ws.onopen = (event) => { console.log(`WebSocket open:`, event); this.setState({ noNetwork: false }); this.resetKeepAlive(); }; this.ws.onmessage = (event) => { let data; try { data = JSON.parse(event.data); } catch (error) { this.setError(error); return; } let update; switch (data.type) { case 'game-update': update = data.update; const error = (update.status !== 'success') ? update.status : undefined; this.updateGame(update); this.updateMessage(); this.setError(error); break; case 'ping': this.ws.send(JSON.stringify({ type: 'pong', timestamp: data.ping })); break; default: console.log(`Unknown event type: ${data.type}`); break; } this.resetKeepAlive(); this.setState({ noNetwork: false }); } this.ws.onerror = (event) => { this.setState({ error: event.message }); console.error(`WebSocket error:`, event); if (!this.websocketReconnect) { this.websocketReconnect = setTimeout(() => { delete this.websocketReconnect; this.connectWebSocket(); }, 1000); } }; this.ws.onclose = (event) => { console.error(`WebSocket close:`, event); this.setState({ noNetowrk: true, error: event.message }); if (!this.websocketReconnect) { this.websocketReconnect = setTimeout(() => { delete this.websocketReconnect; this.connectWebSocket(); }, 1000); } }; } componentDidMount() { this.start = new Date(); console.log(`Mounted: ${base}`); this.connectWebSocket(); 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.updateGame(game); this.updateMessage(); this.setState({ error: "" }); }).catch((error) => { console.error(error); this.setError(error.message); }).then(() => { this.setState({ loading: this.state.loading - 1 }); }); } componentWillUnmount() { if (this.loadTimer) { clearTimeout(this.loadTimer); } if (this.keepAlive) { clearTimeout(this.keepAlive); } if (this.updateSizeTimer) { clearTimeout(this.updateSizeTimer); this.updateSizeTimer = 0; } if (this.errorTimeout) { clearTimeout(this.errorTimeout); } } cardClicked(card) { const game = this.state.game; if (!game) { return; } this.setState({cardActive: card }); } render() { const game = this.state.game, player = game ? game.player : undefined, isTurn = (game && game.turn && game.turn.color === game.color) ? true : false; 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].map(card => this.cardClicked(card)} card={card} table={this} key={`${type}-${card.card}`} type={`${type}-${card.card}`}/>); development.push(
{ cards }
); } } else { development = <>/; } 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 &&
{ 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-resource') !== -1 && } { game && game.state === 'normal' && game.turn && isTurn && game.turn.actions && game.turn.actions.indexOf('steal-resource') !== -1 && } { game && game.turn && !isTurn && (game.state === 'initial-placement' || game.state === 'normal') && (!game.player || !game.player.mustDiscard) && } { this.state.error && this.setState({ error: undefined })} className="Error">
{this.state.error}
}
); } } export default withRouter(props => );