-
-
You want to receive {getLine}.
+ { !player.haveResources &&
You have no resources to participate in this trade. }
+ { player.haveResources &&
+
+
+ You want to receive {getLine}.
+
+
+
+
+
+
+
+
+
+ You are willing to give {giveLine}.
+
+
+ { player.brick > 0 && }
+ { player.wood > 0 && }
+ { player.wheat > 0 && }
+ { player.sheep > 0 && }
+ { player.stone > 0 && }
+
-
-
-
-
-
-
-
-
- You are willing to give {giveLine}.
-
-
- { player.brick > 0 && }
- { player.wood > 0 && }
- { player.wheat > 0 && }
- { player.sheep > 0 && }
- { player.stone > 0 && }
-
-
+ }
Offer
{ isTurn &&
cancel }
diff --git a/client/src/ViewCard.css b/client/src/ViewCard.css
new file mode 100644
index 0000000..9230634
--- /dev/null
+++ b/client/src/ViewCard.css
@@ -0,0 +1,37 @@
+.ViewCard {
+ display: flex;
+ position: absolute;
+ left: 0;
+ right: 40vw;
+ bottom: 0;
+ top: 0;
+ justify-content: center;
+ align-items: center;
+ background: rgba(0,0,0,0.5);
+ z-index: 1000;
+}
+
+.ViewCard .Title {
+ align-self: center;
+ padding: 2px;
+ font-weight: bold;
+ margin-bottom: 0.5em;
+}
+
+.ViewCard .Description {
+ padding: 1em;
+ max-width: 20vw;
+ box-sizing: border-box;
+}
+
+.ViewCard > * {
+/* min-width: 40em;*/
+ display: inline-flex;
+ padding: 0.5em;
+ flex-direction: column;
+}
+
+.ViewCard .Resource {
+ width: 10em; /* 5x7 aspect ratio */
+ height: 14em;
+}
diff --git a/client/src/ViewCard.js b/client/src/ViewCard.js
new file mode 100644
index 0000000..d284213
--- /dev/null
+++ b/client/src/ViewCard.js
@@ -0,0 +1,83 @@
+import React, { useState, useCallback } from "react";
+import "./ViewCard.css";
+import Paper from '@material-ui/core/Paper';
+import Button from '@material-ui/core/Button';
+import Resource from './Resource.js';
+
+const ViewCard = ({table, card}) => {
+ const playCard = (event) => {
+ table.playCard(card);
+ }
+ const close = (event) => {
+ table.closeCard();
+ };
+
+ const capitalize = (string) => {
+ if (string === 'vp') {
+ return 'Victory Point';
+ }
+ if (string === 'army') {
+ return 'Knight';
+ }
+
+ return string.charAt(0).toUpperCase() + string.slice(1);
+ };
+
+ const descriptions = {
+ army: <>When played, you
must move the robber.
+
Steal 1 resource card from the owner of an adjacent settlement or city.
+
You may only play one development card during your turn -- either one
+ knight or one progress card.
>,
+ vp: <>
1 victory point.
+
You only reveal your victory point cards when the game is over, either
+ when you or an opponent reaches 10+ victory points on their turn and declares
+ victory!
>
+ };
+
+ let description = descriptions[card.type];
+
+ let canPlay = false;
+ if (card.type === 'vp') {
+ let points = table.game.player.points;
+ table.game.player.development.forEach(item => {
+ if (item.type === 'vp') {
+ points++;
+ }
+ });
+ canPlay = points >= 10;
+ if (!canPlay && !card.played) {
+ description = <>{description}
You do not have enough victory points to play this card yet.
>;
+ }
+ } else {
+ canPlay = card.turn < table.game.turns;
+ if (!canPlay) {
+ description = <>{description}
You can not play this card until your next turn.
>;
+ }
+ if (canPlay) {
+ canPlay = table.game.player.playedCard !== table.game.turns;
+ }
+ }
+
+ if (card.played) {
+ description = <>{description}
You have already played this card.
>;
+ }
+
+ return (
+
+
+ {capitalize(card.type)}
+
+ { !card.played &&
+ play
+ }
+ close
+
+
+ );
+};
+
+export default ViewCard;
\ No newline at end of file
diff --git a/server/routes/games.js b/server/routes/games.js
index 1d407ef..351b66b 100755
--- a/server/routes/games.js
+++ b/server/routes/games.js
@@ -10,6 +10,10 @@ const express = require("express"),
const { corners } = require("./layout.js");
const layout = require('./layout.js');
+const MAX_SETTLEMENTS = 5;
+const MAX_CITIES = 4;
+const MAX_ROADS = 15;
+
let gameDB;
require("../db/games").then(function(db) {
@@ -115,14 +119,13 @@ const processTies = (players) => {
if (A.order === B.order) {
return B.orderRoll - A.orderRoll;
}
- return A.order - B.order;
+ return B.order - A.order;
});
/* Sort the players into buckets based on their
* order, and their current roll. If a resulting
* roll array has more than one element, then there
* is a tie that must be resolved */
-
let slots = [];
players.forEach(player => {
if (!slots[player.order]) {
@@ -135,14 +138,15 @@ const processTies = (players) => {
});
let ties = false, order = 0;
- slots.forEach((slot) => {
+ /* Reverse from high to low */
+ slots.reverse().forEach((slot) => {
slot.forEach(pips => {
if (pips.length !== 1) {
ties = true;
pips.forEach(player => {
player.orderRoll = 0;
player.order = order;
- player.orderStatus = `Tied for ${order+1}.`;
+ player.orderStatus = `Tied.`;
});
} else {
pips[0].order = order;
@@ -258,7 +262,7 @@ const roll = (game, session) => {
break;
}
- if (player.order || player.orderRoll) {
+ if (player.order && player.orderRoll) {
error = `Player ${name} has already rolled for player order.`;
break;
}
@@ -402,9 +406,9 @@ const processRoll = (game, dice) => {
const getPlayer = (game, color) => {
if (!game) {
return {
- roads: 15,
- cities: 4,
- settlements: 5,
+ roads: MAX_ROADS,
+ cities: MAX_CITIES,
+ settlements: MAX_SETTLEMENTS,
points: 0,
status: "Not active",
lastActive: 0,
@@ -414,6 +418,7 @@ const getPlayer = (game, color) => {
sheep: 0,
wood: 0,
brick: 0,
+ army: 0,
development: []
};
}
@@ -459,16 +464,33 @@ const loadGame = async (id) => {
return;
});
- if (!game) {
- game = createGame(id);
- } else {
+ if (game) {
try {
game = JSON.parse(game);
+ console.log(`Creating backup of games/${id}`);
+ await writeFile(`games/${id}.bk`, JSON.stringify(game));
} catch (error) {
- console.error(error, game);
- return null;
+ console.log(`Attempting to load backup from games/${id}.bk`);
+ game = await readFile(`games/${id}.bk`)
+ .catch(() => {
+ console.error(error, game);
+ });
+ if (game) {
+ try {
+ game = JSON.parse(game);
+ console.log(`Restoring backup to games/${id}`);
+ await writeFile(`games/${id}`, JSON.stringify(game, null, 2));
+ } catch (error) {
+ console.error(error);
+ game = null;
+ }
+ }
}
}
+
+ if (!game) {
+ game = createGame(id);
+ }
if (!game.pipOrder || !game.borderOrder || !game.tileOrder) {
console.log("Shuffling old save file");
@@ -508,6 +530,9 @@ const loadGame = async (id) => {
if (!game.players[color].development) {
game.players[color].development = [];
}
+ if (!game.players[color].army) {
+ game.players[color].army = 0;
+ }
}
games[id] = game;
@@ -595,6 +620,7 @@ const adminActions = (game, action, value) => {
name: next,
color: getColorFromName(game, next)
};
+ game.turns++;
addChatMessage(game, null, `The admin skipped ${name}'s turn.`);
addChatMessage(game, null, `It is ${next}'s turn.`);
break;
@@ -965,8 +991,7 @@ const calculateRoadLengths = (game, session) => {
checkForTies = true;
}
- let longest = game.longestRoad ? game.players[game.longestRoad].roadLength : 4,
- longestPlayers = [];
+ let longest = 4, longestPlayers = [];
for (let key in game.players) {
if (game.players[key].status === 'Not active') {
continue;
@@ -1120,12 +1145,43 @@ const isCompatibleOffer = (player, offer) => {
return;
}
valid = offer.gets.find(item =>
- item.type === give.type &&
+ (item.type === give.type || item.type === 'bank') &&
item.count === give.count) !== undefined;
});
return valid;
};
+const isSameOffer = (player, offer) => {
+ const isBank = offer.name === 'The bank';
+ if (isBank) {
+ return false;
+ }
+ let same = player.gets && player.gives &&
+ player.gets.length === offer.gets.length &&
+ player.gives.length === offer.gives.length;
+
+ if (!same) {
+ return false;
+ }
+
+ player.gets.forEach(get => {
+ if (!same) {
+ return;
+ }
+ same = offer.gets.find(item =>
+ item.type === get.type && item.count === get.count) !== undefined;
+ });
+
+ if (same) player.gives.forEach(give => {
+ if (!same) {
+ return;
+ }
+ same = offer.gives.find(item =>
+ item.type === give.type && item.count === give.count) !== undefined;
+ });
+ return same;
+};
+
const checkOffer = (player, offer) => {
let error = undefined;
offer.gives.forEach(give => {
@@ -1212,7 +1268,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
const name = session.name;
let message, index;
- let corners, corner;
+ let corners, corner, card;
switch (action) {
case "trade":
@@ -1229,6 +1285,11 @@ router.put("/:id/:action/:value?", async (req, res) => {
}
game.turn.actions = [ 'trade' ];
game.turn.limits = {};
+ for (let key in game.players) {
+ game.players[key].gives = [];
+ game.players[key].gets = [];
+ delete game.players[key].offerRejected;
+ }
addChatMessage(game, session, `${name} requested to begin trading negotiations.`);
break;
}
@@ -1254,15 +1315,36 @@ router.put("/:id/:action/:value?", async (req, res) => {
if (error) {
break;
}
+
+ if (isSameOffer(session.player, offer)) {
+ console.log(session.player);
+ error = `You already have a pending offer submitted for ${offerToString(offer)}.`;
+ break;
+ }
+
session.player.gives = offer.gives;
session.player.gets = offer.gets;
+
if (game.turn.name === name) {
+ /* This is a new offer from the active player -- reset everyone's
+ * 'offerRejected' flag */
+ for (let key in game.players) {
+ delete game.players[key].offerRejected;
+ }
game.turn.offer = offer;
}
addChatMessage(game, session, `${session.name} submitted an offer to give ${offerToString(offer)}.`);
break;
}
+ /* Any player can reject an offer */
+ if (value === 'reject') {
+ const offer = req.body;
+ session.player.offerRejected = true;
+ addChatMessage(game, session, `${session.name} rejected ${game.turn.name}'s offer.`);
+ break;
+ }
+
/* Only the active player can accept an offer */
if (value === 'accept') {
if (game.turn.name !== name) {
@@ -1273,6 +1355,11 @@ router.put("/:id/:action/:value?", async (req, res) => {
const offer = req.body;
let target;
+ error = checkOffer(session.player, offer);
+ if (error) {
+ break;
+ }
+
/* Verify that the offer sent by the active player matches what
* the latest offer was that was received by the requesting player */
if (!offer.name || offer.name !== 'The bank') {
@@ -1321,6 +1408,10 @@ router.put("/:id/:action/:value?", async (req, res) => {
player[item.type] -= item.count;
});
+ addChatMessage(game, session, `${session.name} has accepted a trade ` +
+ `offer for ${offerToString(session.player)} ` +
+ `from ${(offer.name === 'The bank') ? 'the bank' : offer.name}.`);
+
delete game.turn.offer;
if (target) {
delete target.gives;
@@ -1331,8 +1422,6 @@ router.put("/:id/:action/:value?", async (req, res) => {
game.turn.actions = [];
- addChatMessage(game, session, `${session.name} has accepted a trade ` +
- `offer from ${(offer.name === 'The bank') ? 'the bank' : offer.name}.`);
break;
}
@@ -1375,6 +1464,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
name: next,
color: getColorFromName(game, next)
};
+ game.turns++;
addChatMessage(game, session, `${name} passed their turn.`);
addChatMessage(game, null, `It is ${next}'s turn.`);
break;
@@ -1444,7 +1534,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
}
});
if (cards.length === 0) {
- addChatMessage(game, session, `Victim did not have any cards to steal.`);
+ addChatMessage(game, session, `${playerNameFromColor(game, value)} did not have any cards to steal.`);
game.turn.actions = [];
game.turn.limits = {};
} else {
@@ -1473,6 +1563,12 @@ router.put("/:id/:action/:value?", async (req, res) => {
error = `You cannot build until you have rolled.`;
break;
}
+
+ if (game.turn && game.turn.roll === 7 && !game.turn.robberDone) {
+ error = `Robber is in action. You can not purchase until all Robber tasks are resolved.`;
+ break;
+ }
+
if (player.stone < 1 || player.wheat < 1 || player.sheep < 1) {
error = `You have insufficient resources to purchase a development card.`;
break;
@@ -1488,9 +1584,78 @@ router.put("/:id/:action/:value?", async (req, res) => {
player.stone--;
player.wheat--;
player.sheep--;
- player.development.push(game.developmentCards.pop());
+ card = game.developmentCards.pop();
+ card.turn = game.turns;
+ player.development.push(card);
break;
+
+ case 'play-card':
+ if (game.state !== 'normal') {
+ error = `You cannot purchase a settlement unless the game is active.`;
+ break;
+ }
+ if (session.color !== game.turn.color) {
+ error = `It is not your turn! It is ${game.turn.name}'s turn.`;
+ break;
+ }
+ if (!game.turn.roll) {
+ error = `You cannot play a card until you have rolled.`;
+ break;
+ }
+ if (game.turn && game.turn.roll === 7 && !game.turn.robberDone) {
+ error = `Robber is in action. You can not play a card until all Robber tasks are resolved.`;
+ break;
+ }
+
+ card = req.body;
+ card = player.development.find(item => item.type == card.type && item.card == card.card);
+ if (!card) {
+ error = `The card you want to play was not found in your hand!`;
+ break;
+ }
+
+ if (player.playedCard === game.turns && card.type !== 'vp') {
+ error = `You can only play one development card per turn!`;
+ break;
+ }
+
+ if (card.played) {
+ error = `You have already played this card.`;
+ break;
+ }
+
+ /* Check if this is a victory point */
+ if (card.type === 'vp') {
+ let points = player.points;
+ player.development.forEach(item => {
+ if (item.type === 'vp') {
+ points++;
+ }
+ });
+ if (points < 10) {
+ error = `You can not play victory point cards until you can reach 10!`;
+ break;
+ }
+ }
+
+ card.played = true;
+ player.playedCard = game.turns;
+ addChatMessage(game, session, `${session.name} played a ${card.type}-${card.card} development card.`);
+
+ if (card.type === 'army') {
+ player.army++;
+ }
+
+ if (player.army > 2 &&
+ (!game.largestArmy || game.players[game.largestArmy].army < player.army)) {
+ if (game.largestArmy !== session.color) {
+ game.largestArmy = session.color;
+ addChatMessage(game, session, `${session.name} now has the largest army (${player.army})!`)
+ }
+ }
+ break;
+
case 'buy-settlement':
if (game.state !== 'normal') {
error = `You cannot purchase a settlement unless the game is active.`;
@@ -1504,6 +1669,12 @@ router.put("/:id/:action/:value?", async (req, res) => {
error = `You cannot build until you have rolled.`;
break;
}
+
+ if (game.turn && game.turn.roll === 7 && !game.turn.robberDone) {
+ error = `Robber is in action. You can not purchase until all Robber tasks are resolved.`;
+ break;
+ }
+
if (player.brick < 1 || player.wood < 1 || player.wheat < 1 || player.sheep < 1) {
error = `You have insufficient resources to build a settlement.`;
break;
@@ -1606,6 +1777,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
}
});
}
+ player.settlements--;
player.maritime = player.banks.map(bank => game.borders[Math.floor(bank / 3) + bank % 3]);
game.turn.actions = ['place-road'];
game.turn.limits = { roads: layout.corners[index].roads }; /* road placement is limited to be near this corner */
@@ -1630,6 +1802,12 @@ router.put("/:id/:action/:value?", async (req, res) => {
error = `You have insufficient resources to build a city.`;
break;
}
+
+ if (game.turn && game.turn.roll === 7 && !game.turn.robberDone) {
+ error = `Robber is in action. You can not purchase until all Robber tasks are resolved.`;
+ break;
+ }
+
if (player.city < 1) {
error = `You have already built all of your cities.`;
break;
@@ -1704,6 +1882,12 @@ router.put("/:id/:action/:value?", async (req, res) => {
error = `You cannot build until you have rolled.`;
break;
}
+
+ if (game.turn && game.turn.roll === 7 && !game.turn.robberDone) {
+ error = `Robber is in action. You can not purchase until all Robber tasks are resolved.`;
+ break;
+ }
+
if (player.brick < 1 || player.wood < 1) {
error = `You have insufficient resources to build a road.`;
break;
@@ -1792,7 +1976,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
color: getColorFromName(game, next)
};
calculateRoadLengths(game, session);
- addChatMessage(game, null, `It is ${next}'s turn. Place a settlement.`);
+ addChatMessage(game, null, `It is ${next}'s turn to place a settlement.`);
} else {
game.turn = {
actions: [],
@@ -1922,12 +2106,8 @@ const sendGame = async (req, res, game, error) => {
/* Enforce game limit of >= 2 players */
if (active < 2 && game.state != 'lobby' && game.state != 'invalid') {
let message = "Insufficient players in game. Setting back to lobby."
- console.log(game);
addChatMessage(game, null, message);
- console.log(message);
- /* It is no one's turn in the lobby */
- delete game.turn;
- game.state = 'lobby';
+ resetGame(game);
}
game.active = active;
@@ -1950,11 +2130,6 @@ const sendGame = async (req, res, game, error) => {
}
game.turn.limits.pips.push(i);
}
- } else {
-/*
- game.turn.limits = {};
- game.turn.actions = [];
-*/
}
}
@@ -1981,6 +2156,30 @@ const sendGame = async (req, res, game, error) => {
lastTime = message.date;
});
+
+ /* Calculate points and determine if there is a winner */
+ for (let key in game.players) {
+ const player = game.players[key];
+ if (player.status === 'Not active') {
+ continue;
+ }
+ player.points = 0;
+ if (key === game.longestRoad) {
+ player.points += 2;
+ }
+ if (key === game.largestArmy) {
+ player.points += 2;
+ }
+ player.points += MAX_SETTLEMENTS - player.settlements;
+ player.points += 2 * (MAX_CITIES - player.cities);
+
+ if (!game.winner && player.points > 10 && session.color === key) {
+ addChatMessage(game, null, `${playerNameFromColor(game, key)} won the game with ${player.points} victory points!`);
+ game.winner = player;
+ game.state = 'winner';
+ }
+ }
+
/* Shallow copy game, filling its sessions with a shallow copy of sessions so we can then
* delete the player field from them */
const reducedGame = Object.assign({}, game, { sessions: {} }),
@@ -2003,13 +2202,25 @@ const sendGame = async (req, res, game, error) => {
console.error(error);
});
+ const player = session.player ? session.player : undefined;
+ if (player) {
+ player.haveResources = player.wheat > 0 ||
+ player.brick > 0 ||
+ player.sheep > 0 ||
+ player.stone > 0 ||
+ player.wood > 0;
+ }
+
+ /* Strip out data that should not be shared with players */
+ delete reducedGame.developmentCards;
+
const playerGame = Object.assign({}, reducedGame, {
timestamp: Date.now(),
status: error ? error : "success",
name: session.name,
color: session.color,
order: (session.color in game.players) ? game.players[session.color].order : 0,
- player: session.player,
+ player: player,
sessions: reducedSessions,
layout: layout
});
@@ -2018,9 +2229,9 @@ const sendGame = async (req, res, game, error) => {
}
const resetGame = (game) => {
- delete game.turn;
game.state = 'lobby';
+ game.turns = 0;
game.placements = {
corners: [],
@@ -2045,15 +2256,14 @@ const resetGame = (game) => {
stone: 0,
brick: 0,
wood: 0,
- roads: 15,
- cities: 4,
- settlements: 5,
+ roads: MAX_ROADS,
+ cities: MAX_CITIES,
+ settlements: MAX_SETTLEMENTS,
points: 0,
development: []
});
}
- game.developmentCards = assetData.developmentCards.slice();
shuffle(game.developmentCards);
for (let i = 0; i < layout.corners.length; i++) {