diff --git a/client/src/Hand.js b/client/src/Hand.js index 304c758..a8d369f 100644 --- a/client/src/Hand.js +++ b/client/src/Hand.js @@ -25,9 +25,11 @@ const Hand = ({buildActive, setBuildActive, setCardActive}) => { const [longestRoad, setLongestRoad] = useState(undefined); const [largestArmy, setLargestArmy] = useState(undefined); const [development, setDevelopment] = useState([]); + const [mostPorts, setMostPorts] = useState(undefined); + const [mostDeveloped, setMostDeveloped] = useState(undefined); const fields = useMemo(() => [ - 'private', 'turn', 'color', 'longestRoad', 'largestArmy' + 'private', 'turn', 'color', 'longestRoad', 'largestArmy', 'mostPorts', 'mostDeveloped' ], []); const onWsMessage = (event) => { const data = JSON.parse(event.data); @@ -49,6 +51,14 @@ const Hand = ({buildActive, setBuildActive, setCardActive}) => { if ('largestArmy' in data.update && largestArmy !== data.update.largestArmy) { setLargestArmy(data.update.largestArmy); } + if ('mostDeveloped' in data.update + && data.update.mostDeveloped !== mostDeveloped) { + setMostDeveloped(data.update.mostDeveloped); + } + if ('mostPorts' in data.update + && data.update.mostPorts !== mostPorts) { + setMostPorts(data.update.mostPorts); + } break; default: break; @@ -121,6 +131,16 @@ const Hand = ({buildActive, setBuildActive, setCardActive}) => {
{ development }
+ {mostDeveloped && mostDeveloped === color && + + } + {mostPorts && mostPorts === color && + + } { longestRoad && longestRoad === color && { console.log(`house-rules - ${field} - `, rules[field]); useEffect(() => { - setGold(rules[field].gold); - setNumber(rules[field].number); - }, [rules, field]); + if (field in rules) { + setGold('gold' in rules[field] ? rules[field].gold : false); + setNumber('number' in rules[field] ? rules[field].number : init); + } + }, [rules, field, init]); const toggleGold = () => { rules[field].gold = !gold; @@ -209,21 +211,35 @@ const HouseRules = ({ houseRulesActive, setHouseRulesActive }) => { useEffect(() => { /* https://icebreaker.com/games/catan-1/feature/catan-house-rules */ setRuleElements([{ - title: `Why you play so slow?`, + title: `Why you play so slowf`, key: `slowest-turn`, - description: `The player with the longest turn so far receives -2VP.`, + description: `The player with the longest turn idle time (longer than 2 minutes) so far loses 2VP.`, element: , implemented: false }, { - title: `More victory points`, - key: `victory-points`, - description: `Customize how many Victory Points are required to win.`, - element: , - implemented: true - }, { + title: `You are so developed`, + key: `most-developed`, + description: + `The player with the most development cards (more than 4) receives 2VP.`, + element: , + implemented: true + }, { + title: `Another round of port`, + key: `port-of-call`, + description: + `The player with the most harbor ports (more than 2) receives 2VP.`, + element: , + implemented: true + }, { + title: `More victory points`, + key: `victory-points`, + description: `Customize how many Victory Points are required to win.`, + element: , + implemented: true + }, { title: `Tiles start facing down`, key: `tiles-start-facing-down`, description: `Resource tiles start upside-down while placing starting settlements.`, diff --git a/client/src/PlayersStatus.css b/client/src/PlayersStatus.css index 9f866ef..d8adf42 100644 --- a/client/src/PlayersStatus.css +++ b/client/src/PlayersStatus.css @@ -6,6 +6,7 @@ pointer-events: none; align-items: flex-end; right: 0; + width: 16rem; background-color: #44444480; padding: 0.25rem; } diff --git a/client/src/PlayersStatus.js b/client/src/PlayersStatus.js index eda00f0..663cddf 100644 --- a/client/src/PlayersStatus.js +++ b/client/src/PlayersStatus.js @@ -9,7 +9,7 @@ import { Placard } from './Placard.js'; import { GlobalContext } from './GlobalContext.js'; const Player = ({ player, onClick, reverse, color, - largestArmy, isSelf, longestRoad }) => { + largestArmy, isSelf, longestRoad, mostPorts, mostDeveloped }) => { if (!player) { return <>; } @@ -33,7 +33,23 @@ const Player = ({ player, onClick, reverse, color, points = <>{player.points}; } - + + const mostPortsPlacard = mostPorts && mostPorts === color ? + : undefined; + + const mostDevelopedPlacard = mostDeveloped && mostDeveloped === color ? + : undefined; + const longestRoadPlacard = longestRoad && longestRoad === color ? }
{points}
- { (largestArmy || longestRoad || armyCards || resourceCards || developmentCards) && <> + { (largestArmy || longestRoad || armyCards || resourceCards || developmentCards || mostPorts || mostDeveloped) && <>
{ !reverse && <> + { mostDevelopedPlacard } + { mostPortsPlacard } { largestArmyPlacard } { longestRoadPlacard } { !largestArmyPlacard && armyCards } @@ -76,6 +94,8 @@ const Player = ({ player, onClick, reverse, color, { !largestArmyPlacard && armyCards } { longestRoadPlacard } { largestArmyPlacard } + { mostPortsPlacard } + { mostDevelopedPlacard } }
} @@ -92,8 +112,10 @@ const PlayersStatus = ({ active }) => { const [color, setColor] = useState(undefined); const [largestArmy, setLargestArmy] = useState(undefined); const [longestRoad, setLongestRoad] = useState(undefined); + const [mostPorts, setMostPorts] = useState(undefined); + const [mostDeveloped, setMostDeveloped] = useState(undefined); const fields = useMemo(() => [ - 'players', 'color', 'longestRoad', 'largestArmy' + 'players', 'color', 'longestRoad', 'largestArmy', 'mostPorts', 'mostDeveloped' ], []); const onWsMessage = (event) => { const data = JSON.parse(event.data); @@ -106,12 +128,22 @@ const PlayersStatus = ({ active }) => { if ('color' in data.update && data.update.color !== color) { setColor(data.update.color); } - if ('longestRoad' in data.update && data.update.longestRoad !== longestRoad) { + if ('longestRoad' in data.update + && data.update.longestRoad !== longestRoad) { setLongestRoad(data.update.longestRoad); } - if ('largestArmy' in data.update && data.update.largestArmy !== largestArmy) { + if ('largestArmy' in data.update + && data.update.largestArmy !== largestArmy) { setLargestArmy(data.update.largestArmy); } + if ('mostDeveloped' in data.update + && data.update.mostDeveloped !== mostDeveloped) { + setMostDeveloped(data.update.mostDeveloped); + } + if ('mostPorts' in data.update + && data.update.mostPorts !== mostPorts) { + setMostPorts(data.update.mostPorts); + } break; default: break; @@ -149,6 +181,8 @@ const PlayersStatus = ({ active }) => { reverse largestArmy={largestArmy} longestRoad={longestRoad} + mostPorts={mostPorts} + mostDeveloped={mostDeveloped} isSelf={active} key={`PlayerStatus-${color}`} color={color}/>; @@ -160,6 +194,8 @@ const PlayersStatus = ({ active }) => { player={players[key]} largestArmy={largestArmy} longestRoad={longestRoad} + mostPorts={mostPorts} + mostDeveloped={mostDeveloped} key={`PlayerStatus-${key}}`} color={key}/>; }); diff --git a/client/src/Table.css b/client/src/Table.css deleted file mode 100755 index 21afa02..0000000 --- a/client/src/Table.css +++ /dev/null @@ -1,137 +0,0 @@ - -.Loading { - position: absolute; - top: 1em; - right: 31em; - width: 3em !important; - height: 3em !important; - z-index: 10010; -} - -.NoNetwork { - position: absolute; - z-index: 10000; - display: flex; - top: 1em; - right: 31em; - width: 3em; - height: 3em; - background-image: url("./assets/no-network.png"); - background-size: contain; - background-position: center; -} - -.Roberta .Pip-Shape:hover, -.Roberta .Pip-Shape { - background-image:url("./assets/woman-robber.png"); -} - -.BottomBar { - display: flex; - position: absolute; - bottom: 0; - left: 0; - right: 30rem; - justify-items: space-between; - justify-content: space-between; - align-items: flex-end; - height: 10.5rem; -} - - -.Display { - display: inline-block; - position: absolute; -} - - -.Placard:hover { - filter: brightness(105%); -} - -.Development:hover { - filter: brightness(150%); -} - -.Development.Selected { - filter: brightness(150%); - top: -1em; -} - - -.Action { - display: flex; - flex-wrap: wrap; - flex: 1 0; - align-items: center; - justify-content: space-evenly; - background-color: rgba(16, 16, 16, 0.25); - padding: 0.25em; -} - -button { - margin: 0.25em; - background-color: white; - border: 1px solid black !important; -} - -.Error { - display: flex; - position: absolute; - top: calc(50vh - 1.5em); - left: calc(0vw + 1em); - right: calc(30vw + 2em); - align-items: center; - justify-content: center; - background-color: yellow; - padding: 1em; - z-index: 10000; -} - -.Message { - display: inline-block; - flex: 1 0; - justify-content: left; - text-align: left; - /* font-size: 12pt;*/ - padding: 0.5em; - user-select: none; -} - -.Message .PlayerColor { - width: 1em; - height: 1em; -} - -.Message div { - display: inline-flex; -} - - -.PlayerName { - padding: 0.5em; - display: flex; - align-items: center; - justify-content: center; - flex-direction: row; -} - -.PlayerName > .nameInput { - margin-right: 1em; - flex: 1; - max-width: 30em; -} - -.PlayerName > Button { - background: lightblue; -} - -.Statistics > div:nth-child(2) { - display: flex; - flex-direction: row; - border: 1px solid black; -} - -.Statistics div:nth-child(2) div { - padding: 0.25em 0.5em; -} diff --git a/client/src/Table.js b/client/src/Table.js deleted file mode 100755 index 97be7a4..0000000 --- a/client/src/Table.js +++ /dev/null @@ -1,816 +0,0 @@ -import React, { useState } 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 Board from './Board.js'; -import Trade from './Trade.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 Chat from './Chat.js'; -import { CircularProgress } from "@material-ui/core"; -import 'moment-timezone'; -import Activities from './Activities.js'; -import Placard from './Placard.js'; -import PlayersStatus from './PlayersStatus.js'; -import { MediaAgent, MediaControl, MediaContext } from './MediaControl.js'; -import { base, assetsPath, getPlayerName, gamesPath } from './Common.js'; - -/* 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 StartButton = ({ table, game }) => { - const startClick = (event) => { - table.setGameState("game-order"); - }; - - return ( - - ); -}; - -/* This needs to take in a mechanism to declare the - * player's active item in the game */ -const Players = ({ table, game }) => { - const toggleSelected = (key) => { - table.setSelected(game.color === key ? "" : key); - } - - const players = []; - - if (!game.id) { - return (<>); - } - - for (let color in game.players) { - const item = game.players[color], inLobby = game.state === 'lobby'; - if (!inLobby && item.status === 'Not active') { - continue; - } - let name = getPlayerName(game.sessions, color), - selectable = game.state === 'lobby' && (item.status === 'Not active' || game.color === color); - players.push(( -
{ inLobby && selectable && toggleSelected(color) }} - key={`player-${color}`}> - {name ? name : 'Available' } - { name && - - } - { !name &&
} -
- )); - } - - return ( - - - { players } - - - ); -} - -console.log("TODO: Convert this to a function component!!!!"); - -class Table extends React.Component { - constructor(props) { - super(props); - this.state = { - message: "", - error: "", - signature: "", - buildActive: false, - cardActive: undefined, - loading: 0, - noNetwork: false, - ws: undefined, - peers: {} - }; - 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.selectResources = this.selectResources.bind(this); - this.buildItem = this.buildItem.bind(this); - - this.loadTimer = null; - this.peers = {}; - this.id = (props.router && props.router.params.id) ? props.router.params.id : 0; - this.setPeers = this.setPeers.bind(this); - } - - setPeers(update) { - for (let key in this.peers) { - if (!(key in update)) { - delete this.peers[key]; - } - } - this.setState({ peers: Object.assign({}, this.peers, update)}); - } - - 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.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}); - } - - selectResources(cards) { - return this.sendAction('select-resources', undefined, cards); - } - - 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); - } - - cancelTrade(trade) { - return this.sendAction('trade', 'cancel', 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.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.state.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); - - /* Only update fields that are changing */ - for (let key in game) { - if (game[key] === this.state[key]) { - delete game[key]; - } - } - console.log(`Updating: `, { ...game }); - this.setState( { ...game } ); - } - - updateMessage() { - const player = (this.state.id && this.state.color) ? this.state.players[this.state.color] : undefined, - name = this.state ? this.state.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.state.state) { - case 'lobby': - message = <>{message}You are in the lobby as {name}.; - if (!this.state.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.state.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.state.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.state.turn) { - if (this.state.turn.roll === 7) { - message = <>{message}Robber was rolled!; - let move = true; - for (let color in this.state.players) { - let name = ''; - for (let i = 0; i < this.state.sessions.length; i++) { - if (this.state.sessions[i].color === color) { - name = this.state.sessions[i].name; - } - } - const discard = this.state.players[color].mustDiscard; - if (discard) { - move = false; - message = <>{message} {name} needs to discard {discard} resources.; - } - } - if (move && (this.state.turn && !this.state.turn.placedRobber)) { - message = <>{message} {this.state.turn.name} needs to move the robber. - } - } else { - message = <>It is {this.state.turn.name}'s turn.; - } - } - break; - default: - message = <>{message}Game state is: {this.state.state}; - break; - } - } - this.setState({ message: message }); - } - - resetKeepAlive(isDead) { - if (isDead) { - console.log(`Short circuiting keep-alive`); - if (this.ws) { - this.ws.close(); - delete this.ws; - } - } else { -// console.log(`${this.game.name} Resetting keep-alive. Last ping: ${(Date.now() - this.lastPing) / 1000}`); - } - - if (this.keepAlive) { - clearTimeout(this.keepAlive); - this.keepAlive = 0; - } else { - console.log(`No keep-alive active`); - } - - this.keepAlive = setTimeout(() => { - console.log(`${this.state.name} No ping after 10 seconds. Last ping: ${(Date.now() - this.lastPing) / 1000}`); - this.setState({ noNetwork: true }); - if (this.ws) { - this.ws.close(); - delete this.ws; - } - this.connectWebSocket(); - }, isDead ? 3000 : 10000); - - if (this.state.noNetwork !== false && !isDead) { - this.setState({ noNetwork: false }); - } else if (this.state.noNetwork !== true && isDead) { - this.setState({ noNetwork: true }); - } - } - - connectWebSocket() { - if (!this.state.id) { - console.log(`Cannot initiate websocket connection while no game is set.`); - this.resetKeepAlive(true); - return; - } - if (this.ws) { - return; - } - 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.state.id}/`; - console.log(`Attempting WebSocket connection to ${new_uri}`); - - this.ws = new WebSocket(new_uri); - this.setState({ ws: this.ws }); - this.lastPing = this.state.timestamp; - - this.ws.addEventListener('message', (event) => { - this.resetKeepAlive(); - - let data; - try { - data = JSON.parse(event.data); - } catch (error) { - this.setError(error); - return; - } - let update; - switch (data.type) { - case 'game-update': - console.log(`Game update received`); - update = data.update; - const error = (update.status !== 'success') ? update.status : undefined; - this.updateGame(update); - this.updateMessage(); - this.setError(error); - break; - case 'ping': - this.lastPing = data.ping; - this.ws.send(JSON.stringify({ type: 'pong', timestamp: data.ping })); - break; - default: - break; - } - }); - - this.ws.addEventListener('error', (event) => { - this.setState({ error: event.message }); - console.error(`${this.state.name} WebSocket error: ${(Date.now() - this.state.lastPing) / 1000}`); - this.resetKeepAlive(true); - }); - - this.ws.addEventListener('close', (event) => { - console.log(`${this.state.name} WebSocket close: ${(Date.now() - this.state.lastPing) / 1000}`); - this.setState({ error: event.message }); - this.resetKeepAlive(true); - }); - - this.ws.addEventListener('open', (event) => { - console.log(`${this.state.name} WebSocket open: Sending game-update request: ${(Date.now() - this.lastPing) / 1000}`); - this.ws.send(JSON.stringify({ type: 'game-update' })); - this.resetKeepAlive(); - }); - } - - componentDidMount() { - this.start = new Date(); - - console.log(`Mounted: ${base}`); - - 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.id = game.id; - - this.updateGame(game); - - /* Connect to the WebSocket (after the game is setup) */ - this.connectWebSocket(); - - this.updateMessage(); - - this.setState({ error: "" }); - }).catch((error) => { - console.error(error); - this.setError(error.message); - }).then(() => { - this.setState({ loading: this.state.loading - 1 }); - }); - } - - buildItem(item) { - return this.sendAction(`buy-${item}`); - } - - componentWillUnmount() { - if (this.ws) { - this.ws.close(); - this.ws = null; - } - if (this.loadTimer) { - clearTimeout(this.loadTimer); - this.loadTimer = 0; - } - if (this.keepAlive) { - clearTimeout(this.keepAlive); - this.keepAlive = 0; - } - if (this.updateSizeTimer) { - clearTimeout(this.updateSizeTimer); - this.updateSizeTimer = 0; - } - if (this.errorTimeout) { - clearTimeout(this.errorTimeout); - this.errorTimeout = 0; - } - } - - cardClicked(card) { - this.setState({cardActive: card }); - } - - render() { - const game = this.state, - player = game ? game.player : undefined, - isTurn = (game && game.turn && game.turn.color === game.color) ? true : false, - showMessage = (game && (game.state === 'lobby' || !game.name)); - - 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] - .sort((A, B) => { - if (A.played) { - return -1; - } - if (B.played) { - return +1; - } - return 0; - }).map(card => this.cardClicked(card)} - card={card} - table={this} - key={`${type}-${card.card}`} - type={`${type}-${card.card}`}/>); - development.push(
{ cards }
); - } - } else { - development = <>/; - } - - if (!this.state.id) { - return <>; - } - - 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 &&
- { showMessage && { 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-resources') !== -1 && - - } - - { game && game.state === 'normal' && - game.turn && - isTurn && - game.turn.actions && game.turn.actions.indexOf('steal-resource') !== -1 && - - } - - { this.state.error && this.setState({ error: undefined })} className="Error">
{this.state.error}
} - -
- ); - } -} -export default withRouter(props => ); diff --git a/server/routes/games.js b/server/routes/games.js index 9ee4482..99787ad 100755 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -487,6 +487,8 @@ const processRoll = (game, session, dice) => { addChatMessage(game, null, `House rule 'Volcanoes have minerals' activated. Players must select which resources to receive from the Volcano!`); game.turn.active = 'volcano'; } + } else { + delete game.turn.select; } } } @@ -590,7 +592,9 @@ const newPlayer = (color) => { color: color, name: "", totalTime: 0, - turnStart: 0 + turnStart: 0, + ports: 0, + developmentCards: 0 }; } @@ -2276,9 +2280,11 @@ const buyDevelopment = (game, session) => { if (game.state !== 'normal') { return `You cannot purchase a development card unless the game is active (${game.state}).`; } + if (session.color !== game.turn.color) { return `It is not your turn! It is ${game.turn.name}'s turn.`; } + if (!game.turn.roll) { return `You cannot build until you have rolled.`; } @@ -2306,6 +2312,7 @@ const buyDevelopment = (game, session) => { player.wheat--; player.sheep--; player.resources = 0; + player.developmentCards++; [ 'wheat', 'brick', 'sheep', 'stone', 'wood' ].forEach(resource => { player.resources += player[resource]; }); @@ -2314,6 +2321,19 @@ const buyDevelopment = (game, session) => { card.turn = game.turns; player.development.push(card); + if (isRuleEnabled(game, 'most-developed')) { + if (player.development.length >= 5 + && (!game.mostDeveloped + || player.developmentCards + > game.players[game.mostDeveloped].developmentCards)) { + if (game.mostDeveloped !== session.color) { + game.mostDeveloped = session.color; + game.mostDevelopmentCards = player.developmentCards; + addChatMessage(game, session, `${session.name} now has the most development cards (${player.developmentCards})!`) + } + } + } + sendUpdateToPlayer(game, session, { private: session.player }); @@ -2321,6 +2341,7 @@ const buyDevelopment = (game, session) => { sendUpdateToPlayers(game, { chat: game.chat, activities: game.activities, + mostDeveloped: game.mostDeveloped, players: getFilteredPlayers(game) }); } @@ -2461,7 +2482,10 @@ const placeSettlement = (game, session, index) => { return `You have requested to place a settlement illegally!`; } /* If this is not a valid road in the turn limits, discard it */ - if (game.turn && game.turn.limits && game.turn.limits.corners && game.turn.limits.corners.indexOf(index) === -1) { + if (game.turn + && game.turn.limits + && game.turn.limits.corners + && game.turn.limits.corners.indexOf(index) === -1) { return `You tried to cheat! You should not try to break the rules.`; } const corner = game.placements.corners[index]; @@ -2518,6 +2542,21 @@ const placeSettlement = (game, session, index) => { if (player.banks.indexOf(type) === -1) { player.banks.push(type); } + + player.ports++; + + if (isRuleEnabled(game, 'port-of-call')) { + console.log(`Checking port-of-call`, player.ports, game.mostPorts); + if (player.ports >= 3 + && (!game.mostPorts + || player.ports > game.mostPortCount)) { + if (game.mostPorts !== session.color) { + game.mostPorts = session.color; + game.mostPortCount = player.ports; + addChatMessage(game, session, `${session.name} now has the most ports (${player.ports})!`) + } + } + } }); } game.turn.actions = []; @@ -2550,6 +2589,7 @@ const placeSettlement = (game, session, index) => { if (player.banks.indexOf(type) === -1) { player.banks.push(type); } + player.ports++; }); } player.settlements--; @@ -2570,6 +2610,7 @@ const placeSettlement = (game, session, index) => { sendUpdateToPlayers(game, { placements: game.placements, activities: game.activities, + mostPorts: game.mostPorts, turn: game.turn, chat: game.chat }); @@ -2812,6 +2853,16 @@ const setRules = (game, session, rules) => { `${getName(session)} has ${rules[rule].enabled ? 'en' : 'dis'}abled the Twelve and Two are Synonyms house rule.`); game.rules[rule] = rules[rule]; break; + case 'most-developed': + addChatMessage(game, null, + `${getName(session)} has ${rules[rule].enabled ? 'en' : 'dis'}abled the Most Developed house rule.`); + game.rules[rule] = rules[rule]; + break; + case 'port-of-call': + addChatMessage(game, null, + `${getName(session)} has ${rules[rule].enabled ? 'en' : 'dis'}abled the Another Round of Port house rule.`); + game.rules[rule] = rules[rule]; + break; case 'tiles-start-facing-down': addChatMessage(game, null, `${getName(session)} has ${rules[rule].enabled ? 'en' : 'dis'}abled the Tiles Start Facing Down house rule.`); @@ -3636,6 +3687,12 @@ const calculatePoints = (game, update) => { if (key === game.largestArmy) { player.points += 2; } + if (key === game.mostPorts) { + player.points += 2; + } + if (key === game.mostDeveloped) { + player.points += 2; + } player.points += MAX_SETTLEMENTS - player.settlements; player.points += 2 * (MAX_CITIES - player.cities); @@ -4460,6 +4517,10 @@ const resetGame = (game) => { longestRoadLength: 0, largestArmy: '', largestArmySize: 0, + mostDeveloped: '', + mostDevelopmentCards: 0, + mostPorts: '', + mostPortCount: 0, winner: undefined, active: 0 });