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
});