From dc2d97196eea14eee8e61cdccb09fc779c3531dd Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Sat, 19 Feb 2022 16:59:49 -0800 Subject: [PATCH] Almost done! Signed-off-by: James Ketrenos --- client/src/Board.css | 2 +- client/src/Board.js | 23 +++- client/src/Resource.css | 5 +- client/src/Table.css | 11 +- client/src/Table.js | 91 +++++++++---- client/src/Trade.css | 6 + client/src/Trade.js | 126 ++++++++++++------ client/src/ViewCard.css | 37 ++++++ client/src/ViewCard.js | 83 ++++++++++++ server/routes/games.js | 286 ++++++++++++++++++++++++++++++++++------ 10 files changed, 559 insertions(+), 111 deletions(-) create mode 100644 client/src/ViewCard.css create mode 100644 client/src/ViewCard.js diff --git a/client/src/Board.css b/client/src/Board.css index 0732d88..ce729ee 100644 --- a/client/src/Board.css +++ b/client/src/Board.css @@ -175,7 +175,7 @@ [data-color='O'] > .Corner-Shape, [data-color='O'] > .Road-Shape { - background-color: rgba(255, 196, 0, 1); + background-color: rgb(255, 128, 0); } [data-color='W'] > .Corner-Shape, diff --git a/client/src/Board.js b/client/src/Board.js index fcb3ca0..885c202 100644 --- a/client/src/Board.js +++ b/client/src/Board.js @@ -70,7 +70,7 @@ const Board = ({ table, game }) => { const Corner = ({corner}) => { const onClick = (event) => { - console.log(`Corner ${corner.index}:`, game.layout.corners[corner.index]); +// console.log(`Corner ${corner.index}:`, game.layout.corners[corner.index]); if (event.currentTarget.getAttribute('data-type') === 'settlement') { table.placeCity(corner.index); } else { @@ -91,7 +91,7 @@ const Board = ({ table, game }) => { const Pip = ({pip}) => { const onClick = (event) => { - console.log(`Pip ${pip.index}:`, game.layout.corners[pip.index]); +// console.log(`Pip ${pip.index}:`, game.layout.corners[pip.index]); table.placeRobber(pip.index); return; }; @@ -377,14 +377,23 @@ const Board = ({ table, game }) => { } } - if (game && game.turn && game.turn.roll) { - let nodes = document.querySelectorAll('.Pip.Active'); + if (game && game.turn) { + let nodes = document.querySelectorAll('.Active'); for (let i = 0; i < nodes.length; i++) { nodes[i].classList.remove('Active'); } - nodes = document.querySelectorAll(`.Pip[data-roll="${game.turn.roll}"]`); - for (let i = 0; i < nodes.length; i++) { - nodes[i].classList.add('Active'); + if (game.turn.roll) { + nodes = document.querySelectorAll(`.Pip[data-roll="${game.turn.roll}"]`); + for (let i = 0; i < nodes.length; i++) { + const index = nodes[i].getAttribute('data-index'); + if (index !== null) { + const tile = document.querySelector(`.Tile[data-index="${index}"]`); + if (tile) { + tile.classList.add('Active'); + } + } + nodes[i].classList.add('Active'); + } } } diff --git a/client/src/Resource.css b/client/src/Resource.css index 1a9ed57..cb8dae7 100644 --- a/client/src/Resource.css +++ b/client/src/Resource.css @@ -1,12 +1,13 @@ .Resource { position: relative; - width: 4.9em; - height: 7.2em; + height: 7em; + width: 5em; display: inline-block; background-position: center; background-repeat: no-repeat; background-size: cover; margin: 0.25em; + cursor: pointer; } .Resource:hover { diff --git a/client/src/Table.css b/client/src/Table.css index 9163928..dfe6078 100755 --- a/client/src/Table.css +++ b/client/src/Table.css @@ -196,6 +196,11 @@ filter: brightness(150%); } +.Development.Selected { + filter: brightness(150%); + top: -1em; +} + .Game { display: flex; position: absolute; @@ -219,9 +224,6 @@ } .Game.lobby { - max-width: 100vw; - width: 100vw; - position: absolute; } /* @@ -366,7 +368,9 @@ display: flex; flex-direction: column; justify-items: space-between; + cursor: pointer; } + .Placard > div { box-sizing: border-box; margin: 0 0.9em; @@ -407,6 +411,7 @@ background-repeat: no-repeat; background-size: cover; margin: 0.25em; + cursor: pointer; } .Action { diff --git a/client/src/Table.js b/client/src/Table.js index c011f14..2d1c70e 100755 --- a/client/src/Table.js +++ b/client/src/Table.js @@ -14,8 +14,7 @@ import { assetsPath, base, getPlayerName, gamesPath } from './Common.js'; import PlayerColor from './PlayerColor.js'; import Dice from './Dice.js'; import Resource from './Resource.js'; - -//import moment from 'moment'; +import ViewCard from './ViewCard.js'; /* Start of withRouter polyfill */ // https://reactrouter.com/docs/en/v6/faq#what-happened-to-withrouter-i-need-it @@ -106,9 +105,10 @@ const Placard = ({table, type, active}) => { ); }; -const Development = ({table, type}) => { +const Development = ({table, type, card, onClick}) => { return ( -
@@ -293,7 +293,7 @@ const GameOrder = ({table}) => {
{item.name}
- { item.orderRoll !== 0 && <>rolled . } + { item.orderRoll !== 0 && <>rolled . {item.orderStatus} } { item.orderRoll === 0 && <>has not rolled yet. {item.orderStatus}}
); @@ -352,7 +352,7 @@ const Action = ({ table }) => { }; const discardClick = (event) => { - const nodes = document.querySelectorAll('.Hand .Selected'), + const nodes = document.querySelectorAll('.Hand .Resource.Selected'), discarding = { wheat: 0, brick: 0, sheep: 0, stone: 0, wood: 0 }; for (let i = 0; i < nodes.length; i++) { discarding[nodes[i].getAttribute("data-type")]++; @@ -386,23 +386,26 @@ const Action = ({ table }) => { return (); } - const inLobby = table.game.state === 'lobby', - player = table.game ? table.game.player : undefined, - hasRolled = (table.game && table.game.turn && table.game.turn.roll) ? true : false, - isTurn = (table.game && table.game.turn && table.game.turn.color === table.game.color) ? true : false, - robberActions = (table.game && table.game.turn && table.game.turn.roll === 7 && !table.game.turn.robberDone); + const game = table.game, + inLobby = game.state === 'lobby', + player = game ? game.player : undefined, + hasRolled = (game && game.turn && game.turn.roll) ? true : false, + isTurn = (game && game.turn && game.turn.color === game.color) ? true : false, + robberActions = (game && game.turn && game.turn.roll === 7 && + !game.turn.robberDone), + haveResources = player ? player.haveResources : false; return ( { inLobby && <> - - } - { table.game.state === 'normal' && <> + + } + { game.state === 'normal' && <> - - - { table.game.turn.roll === 7 && player && player.mustDiscard > 0 && + + + { game.turn.roll === 7 && player && player.mustDiscard > 0 && } @@ -468,7 +471,12 @@ const Players = ({ table }) => { } const name = getPlayerName(table.game.sessions, color), selectable = table.game.state === 'lobby' && (item.status === 'Not active' || table.game.color === color); - let toggleText = name ? name : "Available"; + let toggleText; + if (name) { + toggleText = `${name} has ${item.points} VP`; + } else { + toggleText = "Available"; + } players.push((
{ @@ -626,6 +647,10 @@ class Table extends React.Component { return this.sendAction('trade', 'accept', trade); } + rejectTrade(trade) { + return this.sendAction('trade', 'reject', trade); + } + discard(resources) { return this.sendAction('discard', undefined, resources); } @@ -833,7 +858,7 @@ class Table extends React.Component { break; case 'game-order': if (!player) { - message = <>{message}This game as an observer as  {name}.; + message = <>{message}You are an observer in this game as  {name}.; message = <>{message}You can chat with other players below as {this.game.name}, but cannot play unless players go back to the Lobby.; } else { if (!player.order) { @@ -967,6 +992,14 @@ class Table extends React.Component { } } + cardClicked(card) { + const game = this.state.game; + if (!game) { + return; + } + this.setState({cardActive: card }); + } + render() { const game = this.state.game, player = game ? game.player : undefined @@ -980,15 +1013,25 @@ class Table extends React.Component { let development; if (player) { let stacks = {}; - game.player.development.forEach(item => (item.type in stacks) ? stacks[item.type].push(item.card) : stacks[item.type] = [item.card]); + 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].map(card => ); + const cards = stacks[type].map(card => this.cardClicked(card)} + card={card} + table={this} + key={`${type}-${card.card}`} + type={`${type}-${card.card}`}/>); development.push(
{ cards }
); } } else { development = <>/; } + return (
@@ -1030,6 +1073,10 @@ class Table extends React.Component { }
} + { this.state.cardActive && + + } + { game && game.state === 'game-order' && } diff --git a/client/src/Trade.css b/client/src/Trade.css index 1ca5e3e..3d446a1 100644 --- a/client/src/Trade.css +++ b/client/src/Trade.css @@ -22,6 +22,7 @@ background-color:rgba(224, 224, 224); margin: 0.5em 0; } + .Trade > * { min-width: 40em; display: inline-flex; @@ -29,6 +30,11 @@ flex-direction: column; } +.Trade .Resource { + width: 3.75em; /* 5x7 aspect ratio */ + height: 5.25em; +} + .Trade .PlayerColor { width: 0.5em; height: 0.5em; diff --git a/client/src/Trade.js b/client/src/Trade.js index fa24cb4..5160555 100644 --- a/client/src/Trade.js +++ b/client/src/Trade.js @@ -6,14 +6,13 @@ import Paper from '@material-ui/core/Paper'; import Button from '@material-ui/core/Button'; import Resource from './Resource.js'; -const ResourceCounter = ({type, onCount, max}) => { - const [count, setCount] = useState(0); +const ResourceCounter = ({type, count, onCount, max}) => { + count = count ? count : 0; const plusClicked = (event) => { if (max === undefined || max > count) { if (onCount) { onCount(type, count+1); } - setCount(count+1); } }; const minusClicked = (event) => { @@ -21,7 +20,6 @@ const ResourceCounter = ({type, onCount, max}) => { if (onCount) { onCount(type, count-1); } - setCount(count-1); } }; @@ -77,7 +75,7 @@ const Trade = ({table}) => { } else { setGiveLine(items.join(', ')); } - }, [setGiveLine, setGives]); + }, [setGiveLine, setGives, gives]); const getCount = useCallback((type, count) => { gets[type] = count; @@ -94,12 +92,25 @@ const Trade = ({table}) => { } else { setGetLine(items.join(', ')); } - }, [setGetLine, setGets]); + }, [setGetLine, setGets, gets]); + + const meetClicked = useCallback((offer) => { + const trade = { + gives: offer.gets.slice(), + gets: offer.gives.slice() + }; + console.log(trade); + trade.gives.forEach(give => giveCount(give.type, give.count)); + trade.gets.forEach(get => getCount(get.type, get.count)); + table.offerTrade(trade); + }, [giveCount, getCount]); if (!table.game) { return (<>); } + const game = table.game; + const isTurn = (table.game.turn && table.game.turn.color === table.game.color) ? true : false; const offerClicked = (event) => { @@ -115,7 +126,7 @@ const Trade = ({table}) => { } table.offerTrade(trade); } - + const acceptClicked = (offer) => { table.acceptTrade(offer); }; @@ -124,6 +135,12 @@ const Trade = ({table}) => { table.cancelTrading(); } + /* Non-current player has rejected the active player's + * bid */ + const rejectClicked = (trade) => { + table.rejectTrade(trade); + } + let players = []; for (let color in table.game.players) { const item = table.game.players[color], @@ -134,7 +151,8 @@ const Trade = ({table}) => { color: color, valid: false, gets: item.gets ? item.gets : [], - gives: item.gives ? item.gives : [] + gives: item.gives ? item.gives : [], + offerRejected: item.offerRejected ? true : false }); } } @@ -163,7 +181,14 @@ const Trade = ({table}) => { valid: false }); } + + const player = (table.game && table.game.player) ? table.game.player : undefined; + if (!player) { + return <>; + } + let canAccept = false; + if (table.game.turn.offer) { players.forEach(trade => { let valid = trade.gets.length && trade.gives.length; @@ -187,9 +212,29 @@ const Trade = ({table}) => { }); trade.valid = valid; }); + + canAccept = true; + table.game.turn.offer.gets.forEach(item => { + if (!canAccept) { + canAccept = (item.type in game.player); + } + if (!canAccept) { + return; + } + canAccept = (game.player[item.type] >= item.count); + }); } players = players.map((item, index) => { + if (item.offerRejected) { + return
+ +
{item.name}
+
+ has rejected your offer. +
+
; + } const gets = item.gets.map(get => `${get.count} ${(get.type === 'bank') ? 'of any one resource' : get.type}`) .join(', '), @@ -211,16 +256,18 @@ const Trade = ({table}) => { } { isTurn && } + { !isTurn && item.color === table.game.turn.color && <> + + + }
); }); - const player = (table.game && table.game.player) ? table.game.player : undefined; - if (!player) { - return <>; - } - return (
@@ -230,32 +277,35 @@ const Trade = ({table}) => {
{ players }
-
-
- 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 && } -
-
+ } { isTurn && } 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)}
+
+ +
{description}
+
+ { !card.played && + + } + +
+
+ ); +}; + +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++) {