diff --git a/package.json b/package.json index 8a51a5b..9fe5c6e 100755 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "private": true, "dependencies": { "@material-ui/core": "^4.9.11", + "@material-ui/lab": "^4.0.0-alpha.50", "animakit-expander": "^2.1.4", "bluebird": "^3.5.5", "bootstrap": "^4.4.1", diff --git a/server/app.js b/server/app.js index 4c1ba2f..05c1ada 100755 --- a/server/app.js +++ b/server/app.js @@ -85,7 +85,7 @@ app.use(function(req, res, next){ app.use(session({ store: new SQLiteStore({ db: config.get("sessions.db") }), secret: config.get("sessions.store-secret"), - cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 }, // 1 week + cookie: { maxAge: 21 * 24 * 60 * 60 * 1000 }, // 3 weeks saveUninitialized: false, resave: true })); diff --git a/server/routes/games.js b/server/routes/games.js index 50a916e..9b627d6 100755 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -102,7 +102,60 @@ const games = {}; router.get("/:id", (req, res/*, next*/) => { console.log("GET games/" + req.params.id); if (req.params.id in games) { - return res.status(200).send(games[req.params.id]); + const game = games[req.params.id]; + return res.status(200).send(Object.assign({}, game, { + timestamp: Date.now(), + activePlayer: req.session.activePlayer + })); + } else { + const error = `Game not found: ${req.params.id}`; + return res.status(404).send(error); + } +}); + +router.put("/:id", (req, res/*, next*/) => { + console.log("PUT games/" + req.params.id); + if (req.params.id in games) { + const game = games[req.params.id], + changes = req.body; + + console.log(JSON.stringify(changes, null, 2)); + + for (let change in changes) { + switch (change) { + case "players": + console.log("Player change."); + for (let player in changes.players) { + const playerChange = changes.players[player]; + if (playerChange.name != "") { + game.chat.push({ from: player, date: Date.now(), message: `${player} is now '${playerChange.name}'.` }); + req.session.activePlayer = player; + game.players[player].status = `Just joined`; + } else { + game.chat.push({ from: player, date: Date.now(), message: `${player} is no longer claimed.` }); + req.session.activePlayer = ""; + game.players[player].status = `Not active`; + } + game.players[player].name = playerChange.name; + } + break; + case "chat": + console.log("Chat change."); + game.chat.push({ + from: changes.chat.player, + date: Date.now(), + message: changes.chat.message + }); + if (game.chat.length > 10) { + game.chat.splice(0, game.chat.length - 10); + } + break; + } + } + return res.status(200).send(Object.assign({}, game, { + timestamp: Date.now(), + activePlayer: req.session.activePlayer + })); } else { const error = `Game not found: ${req.params.id}`; return res.status(404).send(error); @@ -117,12 +170,13 @@ router.post("/", (req, res/*, next*/) => { tiles: [], pips: [], borders: [], - tokens: [ - { player: "R", roads: 15, cities: 4, settlements: 5, points: 0 }, - { player: "O", roads: 15, cities: 4, settlements: 5, points: 0 }, - { player: "B", roads: 15, cities: 4, settlements: 5, points: 0 }, - { player: "W", roads: 15, cities: 4, settlements: 5, points: 0 } - ], + tokens: [], + players: { + R: { roads: 15, cities: 4, settlements: 5, points: 0, name: "", status: "Not active" }, + O: { roads: 15, cities: 4, settlements: 5, points: 0, name: "", status: "Not active" }, + B: { roads: 15, cities: 4, settlements: 5, points: 0, name: "", status: "Not active" }, + W: { roads: 15, cities: 4, settlements: 5, points: 0, name: "", status: "Not active" } + }, developmentCards: assetData.developmentCards.slice(), dice: [ 0, 0 ], sheep: 19, @@ -154,7 +208,10 @@ router.post("/", (req, res/*, next*/) => { games[game.id] = game; console.log(`New game created: ${game.id}`); - return res.status(200).send(game); + return res.status(200).send(Object.assign({}, game, { + timestamp: Date.now(), + activePlayer: req.session.activePlayer + })); }); diff --git a/sessions.db b/sessions.db index 7294b5f..aa94acb 100644 Binary files a/sessions.db and b/sessions.db differ diff --git a/src/Board.css b/src/Board.css index cd24d45..02c8e00 100755 --- a/src/Board.css +++ b/src/Board.css @@ -43,8 +43,6 @@ } .Chat { - right: 0; - bottom: 0; padding: 0.5em; width: 30vmax; display: inline-block; @@ -60,6 +58,25 @@ overflow-y: scroll; } +.Players { + padding: 0.5em; + width: 30vmax; + display: inline-block; + opacity: 0.7; +} + +#ChatList { + scroll-behavior: smooth; +} + +.Players > * { + width: 100%; +} + +.Players .nameInput { + flex-grow: 1; +} + .Stack > *:not(:first-child) { margin-left: -4.5em; } diff --git a/src/Board.js b/src/Board.js index 3d5f73a..0309807 100755 --- a/src/Board.js +++ b/src/Board.js @@ -7,9 +7,38 @@ import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; import ListItemText from '@material-ui/core/ListItemText'; import ListItemAvatar from '@material-ui/core/ListItemAvatar'; +import { makeStyles } from '@material-ui/core/styles'; +import { deepOrange, lightBlue, red, grey } from '@material-ui/core/colors'; +import Avatar from '@material-ui/core/Avatar'; +import Switch from '@material-ui/core/Switch'; import Moment from 'react-moment'; import moment from 'moment'; +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(deepOrange[500]), + backgroundColor: deepOrange[500], + }, + W: { + color: theme.palette.getContrastText(grey[100]), + backgroundColor: grey[100], + }, + B: { + color: theme.palette.getContrastText(lightBlue[500]), + backgroundColor: lightBlue[500], + }, +})); + const hexagonRatio = 1.1547005, tileHeight = 0.16, tileWidth = tileHeight * hexagonRatio, @@ -179,49 +208,145 @@ class Resource extends React.Component { } }; -class Chat extends React.Component { - constructor(props) { - super(props); - this.chatInput = this.chatInput.bind(this); - } - - chatInput(event) { +const Chat = ({ game, promoteGameState }) => { + const chatInput = (event) => { console.log(event.target.value); - } + }; - chatKeyPress(event) { + const chatKeyPress = (event) => { if (event.key == "Enter") { console.log(`Send: ${event.target.value}`); + promoteGameState({ chat: { player: game.activePlayer, message: event.target.value }}); event.target.value = ""; } + }; + + const classes = useStyles(); + + useEffect(() => { + const chatList = document.getElementById("ChatList"); + chatList.scrollTop = chatList.scrollHeight - chatList.offsetHeight; + }) + console.log(JSON.stringify(game, null, 2)); + + const timeDelta = game.timestamp - Date.now(); + + const messages = game.chat.map((item, index) => { + const timestamp = moment(item.date - timeDelta).fromNow(); + return ( + + + {item.from} + + + + ); + }); + + return ( + + + { messages } + + )} variant="outlined"/> + + ); +} + +/* This needs to take in a mechanism to declare the + * player's active item in the game */ +const Players = ({ game, promoteGameState }) => { + const [selected, setSelected] = useState(""); + const [name, setName] = useState(""); + + const nameInput = (event) => { + console.log(event.target.value); + }; + + const nameKeyPress = (event) => { + if (event.key == "Enter") { + console.log(`Send: ${event.target.value}`); + setName(event.target.value); + } } - render() { - //this.props.game.messages - const messages =[ { from: "R", date: Date.now(), message: "Hello, world!" } ].map((item, index) => { - const timestamp = moment(item.date).fromNow(); - return ( - - - {item.from} - - - - ); - }); - return ( - - - { messages } - - )} variant="outlined"/> - - ); + useEffect(() => { + const change = { players: {} }; + + /* Joining: selected != "", activePlayer == "", and name != "" */ + if (selected && !game.activePlayer && name != "") { + change.players[selected] = { name: name } + promoteGameState(change) + return; + } + + /* Leaving: selected = "", name = "", activePlayer != "" */ + if (!selected && game.activePlayer != "") { + change.players[game.activePlayer] = { name: "" } + promoteGameState(change) + } + + /* Updating name: selected != "", activePlayer != "", name != "", name != activePlayer.name*/ + if (selected != "" && + game.activePlayer != "" && + name != "" && + game.players[game.activePlayer].name != name) { + change.players[game.activePlayer] = { name: name } + promoteGameState(change) + } + }); + + const toggleSelected = (key) => { + if (selected == key) { + setSelected(""); + setName(""); + } else { + setSelected(key); + } } + const classes = useStyles(); + + const players = []; + for (let key in game.players) { + const item = game.players[key]; + players.push(( + + + {key} + + <> { /* so flex-grow works we put in a fragment */ } + { selected == key && item.name == "" && + input && input.focus()} + disabled={name != item.name} + label="Name" + variant="outlined" autoFocus/> + } + { (selected != key || item.name != "") && + + } + + toggleSelected(key)}/> + + )); + } + + return ( + + + { players } + + + ); } class Board extends React.Component { @@ -233,7 +358,8 @@ class Board extends React.Component { sheep: 0, brick: 0, stone: 0, - wheat: 0 + wheat: 0, + game: null }; this.componentDidMount = this.componentDidMount.bind(this); this.updateDimensions = this.updateDimensions.bind(this); @@ -245,6 +371,7 @@ class Board extends React.Component { this.mouseMove = this.mouseMove.bind(this); this.randomize = this.randomize.bind(this); this.throwDice = this.throwDice.bind(this); + this.promoteGameState = this.promoteGameState.bind(this); this.mouse = { x: 0, y: 0 }; this.radius = 0.317; @@ -284,7 +411,10 @@ class Board extends React.Component { // body: JSON.stringify(data) // body data type must match "Content-Type" header }).then((res) => { if (res.status > 400) { - throw `Unable to load game ${props.match.params.id}`; + const base = document.querySelector("base"); + window.location.href = base ? base.href : "/"; + console.log(`Unable to find game ${props.match.params.id}`); + throw `Unable to find requested game. Staring new one.`; } return res.json(); }).then((game) => { @@ -295,6 +425,7 @@ class Board extends React.Component { } this.game = game; + this.setState({ game: game }); this.pips = Pips(this); this.tiles = Tiles(this); this.table = Table(this); @@ -308,6 +439,29 @@ class Board extends React.Component { }); } + promoteGameState(change) { + console.log("Requesting state change: ", change); + + window.fetch(`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); + alert(`Unable to change state`); + } + return res.json(); + }).then((game) => { + console.log (`Game state changed.`); + this.setState({ game: game }); + }); + } + randomize() { //this.borders = shuffle(this.borders); this.tiles = shuffle(this.tiles); @@ -853,11 +1007,17 @@ class Board extends React.Component { } render() { + const game = this.state.game; return (
this.el = el}> this.canvas = el}>
this.cards = el}> - + { game && + <> + + + + }
In hand