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 { makeStyles } from '@material-ui/core/styles'; import { orange,lightBlue, red, grey } from '@material-ui/core/colors'; import Avatar from '@material-ui/core/Avatar'; import Moment from 'react-moment'; import Board from './Board.js' //import moment from 'moment'; /* 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 base = process.env.PUBLIC_URL; const assetsPath = `${base}/assets`; const gamesPath = `${base}/games`; const useStyles = makeStyles((theme) => ({ root: { display: 'flex', '& > *': { margin: theme.spacing(1), }, }, R: { color: theme.palette.getContrastText(red[500]), backgroundColor: red[500], }, O: { color: theme.palette.getContrastText(orange[500]), backgroundColor: orange[500], }, W: { color: theme.palette.getContrastText(grey[500]), backgroundColor: grey[500], }, B: { color: theme.palette.getContrastText(lightBlue[500]), backgroundColor: lightBlue[500], }, })); const Dice = ({ pips }) => { let name; switch (pips) { case 1: name = 'one'; break; case 2: name = 'two'; break; case 3: name = 'three'; break; case 4: name = 'four'; break; case 5: name = 'five'; break; default: case 6: name = 'six'; break; } return ( {name} ); } const PlayerColor = ({ color }) => { const classes = useStyles(); return ( ); }; const diceSize = 0.05, dice = [ { pips: 0, jitter: 0, angle: 0 }, { pips: 0, jitter: 0, angle: 0 } ]; class Placard extends React.Component { render() { return (
); } }; class Development extends React.Component { render() { const array = []; for (let i = 0; i < this.props.count; i++) { if (this.props.type.match(/-$/)) { array.push(i + 1);//Math.ceil(Math.random() * this.props.max)); } else { array.push(""); } } return (
{ React.Children.map(array, i => (
)) }
); } }; class Resource extends React.Component { render() { const array = new Array(Number(this.props.count ? this.props.count : 0)); return ( <> { array.length > 0 &&
{ React.Children.map(array, i => (
)) }
} ); } }; const Chat = ({ table, promoteGameState }) => { const [lastTop, setLastTop] = useState(0), [autoScroll, setAutoscroll] = useState(true), [scrollTime, setScrollTime] = useState(0); const chatInput = (event) => { }; const chatKeyPress = (event) => { if (event.key === "Enter") { if (!autoScroll) { setAutoscroll(true); } promoteGameState({ chat: { player: table.game.color ? table.game.color : undefined, message: 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) => { /* If the date is in the future, set it to now */ const name = getPlayerName(table.game.sessions, item.from), from = name ? `${name}, ` : ''; return ( {from} Date.now() ? Date.now() : item.date} interval={1000}/>)} /> ); }); const name = table.game ? table.game.name : "Why no game?"; return ( { messages } )} variant="outlined"/> ); } const StartButton = ({ table }) => { const startClick = (event) => { table.setGameState("game-order").then((state) => { table.game.state = state; }); }; return ( ); }; const Action = ({ table }) => { const newTableClick = (event) => { return table.shuffleTable(); }; const rollClick = (event) => { table.throwDice(); } const passClick = (event) => { } if (!table.game) { console.log("Why no game?"); return (); } return ( { table.game.state === 'lobby' && <> } { table.game.state === 'game-order' && } { table.game.state === 'active' && <> } ); } 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 = () => { console.log(`Send: ${name}`); if (name !== table.game.name) { table.setPlayerName(name); } else { table.setState({ pickName: false, error: "" }); } } const nameKeyPress = (event) => { if (event.key === "Enter") { sendName(); } } return ( ); }; const getPlayerName = (sessions, color) => { for (let i = 0; i < sessions.length; i++) { const session = sessions[i]; if (session.color === color) { return session.name; } } return null; } /* This needs to take in a mechanism to declare the * player's active item in the game */ const Players = ({ table }) => { const toggleSelected = (key) => { console.log('toggle'); 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 = name ? name : "Available"; players.push((
{ inLobby && selectable && toggleSelected(color) }} key={`player-${color}`}> { item.status + ' ' } { item.status !== 'Not active' && Date.now() ? Date.now() : item.lastActive}/>} )} /> { !inLobby && table.game.color === color && }
)); } 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: "" }; this.componentDidMount = this.componentDidMount.bind(this); this.updateDimensions = this.updateDimensions.bind(this); this.throwDice = this.throwDice.bind(this); this.promoteGameState = this.promoteGameState.bind(this); this.resetGameLoad = this.resetGameLoad.bind(this); this.loadGame = this.loadGame.bind(this); this.rollDice = this.rollDice.bind(this); this.setGameState = this.setGameState.bind(this); this.shuffleTable = this.shuffleTable.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.gameSignature = this.gameSignature.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; } setSelected(key) { if (this.loadTimer) { window.clearTimeout(this.loadTimer); this.loadTimer = null; } return window.fetch(`${base}/api/v1/games/${this.state.game.id}/player-selected/${key}`, { method: "PUT", cache: 'no-cache', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' } }).then((res) => { if (res.status >= 400) { throw new Error(`Unable to set selected player!`); } return res.json(); }).then((game) => { const error = (game.status !== 'success') ? game.status : undefined; this.updateGame(game); this.updateMessage(); this.setState({ error: error }); }).catch((error) => { console.error(error); this.setState({error: error.message}); }).then(() => { this.resetGameLoad(); }); } setPlayerName(name) { if (this.loadTimer) { window.clearTimeout(this.loadTimer); this.loadTimer = null; } return window.fetch(`${base}/api/v1/games/${this.state.game.id}/player-name/${name}`, { method: "PUT", cache: 'no-cache', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' } }).then((res) => { if (res.status >= 400) { throw new Error(`Unable to set player name!`); } return res.json(); }).then((game) => { let message; if (game.status !== 'success') { message = game.status; } else { this.setState({ pickName: false }); } this.updateGame(game); this.updateMessage(); this.setState({ error: message}); }).catch((error) => { console.error(error); this.setState({error: error.message}); }).then(() => { this.resetGameLoad(); }); } shuffleTable() { if (this.loadTimer) { window.clearTimeout(this.loadTimer); this.loadTimer = null; } return window.fetch(`${base}/api/v1/games/${this.state.game.id}/shuffle`, { method: "PUT", cache: 'no-cache', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' } }).then((res) => { if (res.status >= 400) { throw new Error(`Unable to shuffle!`); } return res.json(); }).then((game) => { console.log (`Table shuffled!`); this.updateGame(game); this.updateMessage(); this.setState({ error: "Table shuffled!" }); }).catch((error) => { console.error(error); this.setState({error: error.message}); }).then(() => { this.resetGameLoad(); }); } rollDice() { if (this.loadTimer) { window.clearTimeout(this.loadTimer); this.loadTimer = null; } return window.fetch(`${base}/api/v1/games/${this.state.game.id}/roll`, { 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 roll dice`); } return res.json(); }).then((game) => { const error = (game.status !== 'success') ? game.status : undefined; if (error) { game.dice = [ game.order ]; } this.updateGame(game); this.updateMessage(); this.setState({ /*game: { ...this.state.game, dice: game.dice },*/ error: error } ); }).catch((error) => { console.error(error); this.setState({error: error.message}); }).then(() => { this.resetGameLoad(); return this.game.dice; }); } loadGame() { if (this.loadTimer) { window.clearTimeout(this.loadTimer); this.loadTimer = null; } if (!this.state.game) { console.error('Attempting to loadGame with no game set'); return; } return window.fetch(`${base}/api/v1/games/${this.state.game.id}`, { method: "GET", cache: 'no-cache', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' } }).then((res) => { if (res.status >= 400) { console.log(res); throw new Error(`Server temporarily unreachable.`); } return res.json(); }).then((game) => { const error = (game.status !== 'success') ? game.status : undefined; //console.log (`Game ${game.id} loaded ${moment().format()}.`); this.updateGame(game); this.updateMessage(); this.setState({ error: error }); }).catch((error) => { console.error(error); this.setState({error: error.message}); }).then(() => { this.resetGameLoad(); }); } resetGameLoad() { if (this.loadTimer) { window.clearTimeout(this.loadTimer); this.loadTimer = 0; } this.loadTimer = window.setTimeout(this.loadGame, 1000); } promoteGameState(change) { console.log("Requesting state change: ", change); if (this.loadTimer) { window.clearTimeout(this.loadTimer); this.loadTimer = null; } return window.fetch(`${base}/api/v1/games/${this.state.game.id}`, { method: "PUT", cache: 'no-cache', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(change) }).then((res) => { if (res.status >= 400) { console.error(res); throw new Error(`Unable to change state`); } return res.json(); }).then((game) => { this.updateGame(game); this.setState({ error: "" }); }).catch((error) => { console.error(error); this.setState({error: error.message}); }).then(() => { this.resetGameLoad(); }); } setGameState(state) { if (this.loadTimer) { window.clearTimeout(this.loadTimer); this.loadTimer = null; } 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(); this.setState({ /*game: { ...this.state.game, state: game.state }, */error: `Game state now ${game.state}.` }); }).catch((error) => { console.error(error); this.setState({error: error.message}); }).then(() => { this.resetGameLoad(); return this.game.state; }); } throwDice() { dice[0].pips = dice[1].pips = 0; return this.rollDice().then((roll) => { roll.forEach((value, index) => { dice[index] = { pips: value, angle: Math.random() * Math.PI * 2, jitter: (Math.random() - 0.5) * diceSize * 0.125 }; }); if (this.game.state !== 'active') { return; } const sum = dice[0].pips + dice[1].pips; if (sum === 7) { /* Robber! */ if (this.state.total > 7) { let half = Math.ceil(this.state.total * 0.5); this.setState({ total: this.state.total - half}); while (half) { switch (Math.floor(Math.random() * 5)) { case 0: if (this.state.wood) { this.setState({ wood: this.state.wood - 1}); half--; } break; case 1: if (this.state.sheep) { this.setState({ sheep: this.state.sheep - 1}); half--; } break; case 2: if (this.state.stone) { this.setState({ stone: this.state.stone - 1}); half--; } break; case 3: if (this.state.brick) { this.setState({ brick: this.state.brick - 1}); half--; } break; case 4: default: if (this.state.wheat) { this.setState({ wheat: this.state.wheat - 1}); half--; } break; } } } } else { this.tiles.forEach((tile) => { if (tile.pip.roll !== sum) { return; } this.setState({ [tile.type]: this.state[tile.type] + 1}); this.setState({ total: this.state.total + 1 }); }); } this.setState({ total: this.state.total, wood: this.state.wood, sheep: this.state.sheep, stone: this.state.stone, brick: this.state.brick, wheat: this.state.wheat }); }).catch((error) => { console.error(error); }); } updateDimensions() { const hasToolbar = false; if (this.updateSizeTimer) { clearTimeout(this.updateSizeTimer); } this.updateSizeTimer = setTimeout(() => { const container = document.getElementById("root"), offset = hasToolbar ? container.firstChild.offsetHeight : 0, height = window.innerHeight - offset; this.offsetY = offset; this.width = window.innerWidth; this.height = height; if (this.cards && this.cards.style) { this.cards.style.top = `${offset}px`; this.cards.style.width = `${this.width}px`; this.cards.style.height = `${this.heigh}tpx`; } this.updateSizeTimer = 0; }, 250); } gameSignature(game) { if (!game) { return ""; } const signature = game.borderOrder.map(border => Number(border).toString(16)).join('') + '-' + game.pipOrder.map(pip => Number(pip).toString(16)).join('') + '-' + game.tileOrder.map(tile => Number(tile).toString(16)).join(''); return signature; }; updateGame(game) { if (this.state.signature !== this.gameSignature(game)) { game.signature = this.gameSignature(game); } // console.log("Update Game", game); this.setState( { game: 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 need to pick your color.; } 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}This game as an observer 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.; message = <>{message}
THIS IS THE END OF THE FUNCTIONALITY SO FAR; } } 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; } else { message = <>{message}
THIS IS THE END OF THE FUNCTIONALITY SO FAR; } break; case null: case undefined: case '': message = <>{message}The game is in a wonky state. Sorry :(; break; default: message = <>{message}Game state is: {this.game.state}; break; } } this.setState({ message: message }); } componentDidMount() { this.start = new Date(); 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"; } 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.setState({ error: 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) => { // console.log (`Game ${game.id} loaded ${moment().format()}.`); if (!this.id) { history.push(`${gamesPath}/${game.id}`); } this.updateGame(game); this.updateMessage(); this.setState({ error: "" }); }).catch((error) => { console.error(error); this.setState({error: error.message}); }).then(() => { this.resetGameLoad(); }); setTimeout(this.updateDimensions, 1000); } componentWillUnmount() { if (this.loadTimer) { clearTimeout(this.loadTimer); } if (this.updateSizeTimer) { clearTimeout(this.updateSizeTimer); this.updateSizeTimer = 0; } } render() { const game = this.state.game; return (
this.el = el}> { game &&
{ this.state.message } {(this.state.pickName || !game.name) && } {(!this.state.pickName && game.name) && <> }
}
this.cards = el}> { game && game.state === "active" && <>
In hand
Available to play
Points
Stats
Points: 7
Cards: {this.state.total}
Roads remaining: 4
Longest road: 7
Cities remaining: 4
Settlements remaining: 5
}
{ this.state.error &&
{this.state.error}
}
); } } export default withRouter(props => );