1
0
James Ketrenos 7af8d02802 Game now sets up correct tile and pip order!
Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
2022-02-02 20:33:23 -08:00

1014 lines
29 KiB
JavaScript
Executable File

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 (
<Component
{...props}
router={{ location, navigate, params }}
/>
);
}
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 (
<img alt={name} className="Dice" src={`${assetsPath}/dice-six-faces-${name}.svg`}/>
);
}
const PlayerColor = ({ color }) => {
const classes = useStyles();
return (
<Avatar className={['PlayerColor', classes[color]].join(' ')}/>
);
};
const diceSize = 0.05,
dice = [ {
pips: 0,
jitter: 0,
angle: 0
}, {
pips: 0,
jitter: 0,
angle: 0
} ];
class Placard extends React.Component {
render() {
return (
<div className="Placard"
style={{backgroundImage:`url(${assetsPath}/gfx/placard-${this.props.type}.png)`}}>
</div>
);
}
};
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 (
<div className="Stack">
{ React.Children.map(array, i => (
<div className="Development"
style={{backgroundImage:`url(${assetsPath}/gfx/card-${this.props.type}${i}.png)`}}>
</div>
)) }
</div>
);
}
};
class Resource extends React.Component {
render() {
const array = new Array(Number(this.props.count ? this.props.count : 0));
return (
<>
{ array.length > 0 &&
<div className="Stack">
{ React.Children.map(array, i => (
<div className="Resource"
style={{backgroundImage:`url(${assetsPath}/gfx/card-${this.props.type}.png)`}}>
</div>
)) }
</div>
}
</>
);
}
};
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 (
<ListItem key={`msg-${item.date}`}>
<PlayerColor color={item.from}/>
<ListItemText primary={item.message}
secondary={(<>{from}
<Moment fromNow date={item.date > Date.now() ?
Date.now() : item.date} interval={1000}/></>)} />
</ListItem>
);
});
const name = table.game ? table.game.name : "Why no game?";
return (
<Paper className="Chat">
<List id="ChatList" onScroll={chatScroll}>
{ messages }
</List>
<TextField className="chatInput"
disabled={!name}
onChange={chatInput}
onKeyPress={chatKeyPress}
label={(<Moment format="h:mm:ss" interval={1000}/>)} variant="outlined"/>
</Paper>
);
}
const StartButton = ({ table }) => {
const startClick = (event) => {
table.setGameState("game-order").then((state) => {
table.game.state = state;
});
};
return (
<Button disabled={!table.game.color || table.game.active < 2} onClick={startClick}>Start game</Button>
);
};
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 (<Paper className="Action"/>);
}
return (
<Paper className="Action">
{ table.game.state === 'lobby' && <>
<StartButton table={table}/>
<Button disabled={!table.game.color} onClick={newTableClick}>New table</Button>
<Button disabled={table.game.color ? true : false} onClick={() => {table.setState({ pickName: true})}}>Change name</Button> </> }
{ table.game.state === 'game-order' &&
<Button disabled={table.game.order !== 0} onClick={rollClick}>Roll Dice</Button> }
{ table.game.state === 'active' && <>
<Button onClick={rollClick}>Roll Dice</Button>
<Button disabled onClick={passClick}>Pass Turn</Button>
</> }
</Paper>
);
}
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 (
<Paper className="PlayerName">
<TextField className="nameInput"
onChange={nameChange}
onKeyPress={nameKeyPress}
label="Enter your name"
variant="outlined"
value={name}
/>
<Button onClick={() => sendName()}>Set</Button>
</Paper>
);
};
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((
<div
data-selectable={selectable}
data-selected={table.game.color === color}
className="PlayerEntry"
onClick={() => { inLobby && selectable && toggleSelected(color) }}
key={`player-${color}`}>
<PlayerColor color={color}/>
<ListItemText primary={toggleText} secondary={(
<>
{ item.status + ' ' }
{ item.status !== 'Not active' && <Moment fromNow date={item.lastActive > Date.now() ? Date.now() : item.lastActive}/>}
</>)} />
{ !inLobby && table.game.color === color &&
<Button onClick={() => toggleSelected(color)}>Quit</Button>
}
</div>
));
}
return (
<Paper className="Players">
<List className="PlayerSelector">
{ players }
</List>
</Paper>
);
}
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&nbsp;<b>ENTER</b>&nbsp;or select &nbsp;<b>SET</b>.</>;
} else {
switch (this.game && this.game.state) {
case 'lobby':
message = <>{message}You are in the lobby as&nbsp;<b>{name}</b>.</>;
if (!this.game.color) {
message = <>{message}You need to pick your color.</>;
} else {
message = <>{message}You have selected <PlayerColor color={this.game.color}/>.</>;
}
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 <StartButton table={this}/>.</>;
} else {
message = <>{message}There are enough players to start the game. Select <StartButton table={this}/> when ready.</>;
}
break;
case 'game-order':
if (!player) {
message = <>{message}This game as an observer as &nbsp;<b>{name}</b>.</>;
message = <>{message}You can chat with other players below as&nbsp;<b>{this.game.name}</b>, but cannot play unless players go back to the Lobby.</>;
} else {
if (!player.order) {
message = <>{message}You need to roll for game order. Click&nbsp;<b>Roll Dice</b>&nbsp;below.</>;
} else {
message = <>{message}You rolled <Dice pips={player.order}/> for game order. Waiting for all players to roll.</>;
message = <>{message}<br/><b>THIS IS THE END OF THE FUNCTIONALITY SO FAR</b></>;
}
}
break;
case 'active':
if (!player) {
message = <>{message}This game is no longer in the lobby.<br/><b>TODO: Override game state to allow Lobby mode while in-game</b></>;
} else {
message = <>{message}<br/><b>THIS IS THE END OF THE FUNCTIONALITY SO FAR</b></>;
}
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 (
<div className="Table" ref={el => this.el = el}>
<Board game={game}/>
{ game && <div className={'Game ' + game.state}>
<Paper className="Message">{ this.state.message }</Paper>
{(this.state.pickName || !game.name) && <PlayerName table={this}/> }
{(!this.state.pickName && game.name) && <>
<Players table={this}/>
<Chat table={this} promoteGameState={this.promoteGameState}/>
<Action table={this}/>
</> }
</div> }
<div className="Cards" ref={el => this.cards = el}>
{ game && game.state === "active" && <>
<div>In hand</div>
<div className="Hand">
<Resource type="wood" count={this.state.wood}/>
<Resource type="wheat" count={this.state.wheat}/>
<Resource type="stone" count={this.state.stone}/>
<Resource type="brick" count={this.state.brick}/>
<Resource type="sheep" count={this.state.sheep}/>
</div>
<div>Available to play</div>
<div className="Hand">
<Development type="monopoly" count="1"/>
<Development type="army-" max="14" count="4"/>
<div className="Stack">
<Development type="vp-library" count="1"/>
<Development type="vp-market" count="1"/>
</div>
</div>
<div>Points</div>
<div className="Hand">
<div className="Stack">
<Development type="vp-library" count="1"/>
<Development type="vp-palace" count="1"/>
<Development type="army-" max="14" count="6"/>
</div>
</div>
<div className="Hand">
<Placard type="largest-army" count="1"/>
<Placard type="longest-road" count="1"/>
</div>
<div className="Statistics">
<div>Stats</div>
<div>
<div>Points: 7</div>
<div>Cards: {this.state.total} </div>
<div>Roads remaining: 4</div>
<div>Longest road: 7</div>
<div>Cities remaining: 4</div>
<div>Settlements remaining: 5</div>
</div>
</div>
</> }
</div>
{ this.state.error && <Paper className="Error"><div>{this.state.error}</div></Paper> }
</div>
);
}
}
export default withRouter(props => <Table {...props}/>);