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.toString()) { 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 ( ); }; 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 }) => { 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); } 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) => { /* If the date is in the future, set it to now */ const name = item.from ? item.from : item.color, from = name ? `${name}, ` : ''; let message; 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 ( {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 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.orderRoll === 0 && <>has not rolled yet. {item.orderStatus}}
); return (
{ table.game &&
Game Order
{ players }
}
); }; const Action = ({ table }) => { const newTableClick = (event) => { return table.shuffleTable(); }; 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 inLobby = table.game.state === 'lobby'; return ( { inLobby && <> } { table.game.state === 'normal' && <> } { !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.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) => { 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}/>} )} />
)); } 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.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.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.gameSignature = this.gameSignature.bind(this); this.sendAction = this.sendAction.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; } sendAction(action, value, extra) { if (this.loadTimer) { window.clearTimeout(this.loadTimer); this.loadTimer = null; } return window.fetch(`${base}/api/v1/games/${this.state.game.id}/${action}/${value ? 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.setState({ error: error }); }).catch((error) => { console.error(error); this.setState({error: error.message}); }).then(() => { this.resetGameLoad(); }); } setSelected(key) { return this.sendAction('player-selected', key); } sendChat(message) { return this.sendAction('chat', undefined, {message: message}); } setPlayerName(name) { return this.sendAction('player-name', name) .then(() => { this.setState({ pickName: false }); }); } shuffleTable() { return this.sendAction('shuffle') .then(() => { this.setState({ error: "Table shuffled!" }); }); } passTurn() { return this.sendAction('pass'); }; rollDice() { return this.sendAction('roll'); } 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; 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); } 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(); }).catch((error) => { console.error(error); this.setState({error: error.message}); }).then(() => { this.resetGameLoad(); return this.game.state; }); } placeSettlement(settlement) { return this.sendAction('place-settlement', settlement); } placeRoad(road) { return this.sendAction('place-road', road); } throwDice() { return this.rollDice(); if (0) { if (this.game.state !== 'active') { return; } const sum = 0;//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 }); } }; 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 (
{ game &&
{ this.state.message } {(this.state.pickName || !game.name) && } {(!this.state.pickName && game.name) && <> }
} { game && game.state === 'game-order' && } { game && game.turn && game.turn.color !== game.color && (game.state === 'initial-placement' || game.state === 'normal') && } { game && game.showCards &&
{ 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 => );