diff --git a/client/src/Table.css b/client/src/Table.css index 0863943..9299f50 100755 --- a/client/src/Table.css +++ b/client/src/Table.css @@ -395,6 +395,13 @@ background-size: cover; margin: 0.25em; display: inline-block; + display: flex; + flex-direction: column; + justify-items: space-between; +} +.Placard > div { + box-sizing: border-box; + margin: 0 0.9em; } .Placard:not([disabled]) { cursor: pointer; @@ -404,6 +411,24 @@ transform-origin: 100% 100%; transform: scale(1.5); } +.Placard > div:nth-child(1) { + height: 15.5%; +} +.Placard > div:nth-child(2), +.Placard > div:nth-child(3), +.Placard > div:nth-child(4), +.Placard > div:nth-child(5) { + height: 15.25%; +} +.Placard > div:nth-child(6) { + flex: 1; +} +.Placard > div:hover:nth-child(2), +.Placard > div:hover:nth-child(3), +.Placard > div:hover:nth-child(4), +.Placard > div:hover:nth-child(5) { + background-color: #ffffff40; +} .Development { position: relative; diff --git a/client/src/Table.js b/client/src/Table.js index 0043aae..5c701bf 100755 --- a/client/src/Table.js +++ b/client/src/Table.js @@ -93,21 +93,63 @@ const PlayerColor = ({ color }) => { ); }; -const Placard = ({table, type}) => { +const Placard = ({table, type, active}) => { + const dismissClicked = (event) => { + table.setState({ buildActive: false }); + } + const buildClicked = (event) => { - if (table) { - table.buildClicked(event); + if (!table.state.buildActive) { + table.setState({ buildActive: true }); } }; - return ( -
{ + table.buyRoad(); + table.setState({ buildActive: false }); + }; + + const settlementClicked = (event) => { + table.buySettlement(); + table.setState({ buildActive: false }); + }; + + const cityClicked = (event) => { + table.setState({ buildActive: false }); + }; + + const developmentClicked = (event) => { + table.setState({ buildActive: false }); + }; + + let buttons; + switch (active ? type : undefined) { + case 'orange': + case 'red': + case 'white': + case 'blue': + buttons = <> +
+
+
+
+
+
+ ; + break; + default: + buttons = <>; + break; + } + + return ( +
+ >{buttons}
); }; @@ -423,7 +465,9 @@ const Action = ({ table }) => { } const inLobby = table.game.state === 'lobby', - player = table.game ? table.game.player : undefined; + player = table.game ? table.game.player : undefined, + hasRolled = table.game && table.game.turn && table.game.turn.roll, + isTurn = table.game && table.game.turn && table.game.turn.color === table.game.color; return ( @@ -432,13 +476,13 @@ const Action = ({ table }) => { } { table.game.state === 'normal' && <> - - - + + + { table.game.turn.roll === 7 && player && player.mustDiscard > 0 && } - + } { !inLobby && @@ -553,7 +597,8 @@ class Table extends React.Component { game: null, message: "", error: "", - signature: "" + signature: "", + buildActive: false }; this.componentDidMount = this.componentDidMount.bind(this); this.updateDimensions = this.updateDimensions.bind(this); @@ -740,34 +785,23 @@ class Table extends React.Component { buildClicked(event) { console.log("Build clicked"); - const game = this.state.game, - player = game ? game.player : undefined - let color; - switch (game ? game.color : undefined) { - case "O": color = "orange"; break; - case "R": color = "red"; break; - case "B": color = "blue"; break; - case "W": color = "white"; break; - } - - const nodes = document.querySelectorAll(`.Placard.Selected`); - for (let i = 0; i < nodes.length; i++) { - nodes[i].classList.remove('Selected'); - } - const placard = document.querySelector(`.Placard[data-type="${color}"]`); - if (placard) { - placard.classList.add('Selected'); - } + this.setState({ buildActive: this.state.buildActive ? false : true }); }; placeRobber(robber) { return this.sendAction('place-robber', robber); }; + buySettlement() { + return this.sendAction('buy-settlement'); + } placeSettlement(settlement) { return this.sendAction('place-settlement', settlement); } + buyRoad() { + return this.sendAction('buy-road'); + } placeRoad(road) { return this.sendAction('place-road', road); } @@ -1055,7 +1089,10 @@ class Table extends React.Component {
- +
}
diff --git a/server/routes/games.js b/server/routes/games.js index fd6e660..8448976 100755 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -7,6 +7,7 @@ const express = require("express"), accessSync = fs.accessSync, randomWords = require("random-words"); +const { corners } = require("./layout.js"); const layout = require('./layout.js'); let gameDB; @@ -507,7 +508,7 @@ const clearPlayer = (player) => { } const adminActions = (game, action, value) => { - let color, player; + let color, player, parts, session; switch (action) { case "state": @@ -518,9 +519,29 @@ const adminActions = (game, action, value) => { break; } break; + + case "give": + parts = value.match(/^([^-]+)-([0-9]+)$/); + if (!parts) { + return `Unable to parse give request.`; + } + for (let id in game.sessions) { + if (game.sessions[id].name === game.turn.name) { + session = game.sessions[id]; + } + } + if (!session) { + return `Unable to determine current player turn to give resources.`; + } + if (!(parts[1] in session.player)) { + return `Invalid resource request.`; + } + session.player[parts[1]] += parseInt(parts[2]); + addChatMessage(game, null, `Admin gave ${parseInt(parts[2])} ${parts[1]} to ${game.turn.name}.`); + break; case "roll": - let parts = value.match(/^([1-6])(-([1-6]))?$/); + parts = value.match(/^([1-6])(-([1-6]))?$/); if (!parts) { return `Unable to parse roll request.`; } @@ -528,7 +549,6 @@ const adminActions = (game, action, value) => { if (parts[3]) { dice.push(parseInt(parts[3])); } - let session; for (let id in game.sessions) { if (game.sessions[id].name === game.turn.name) { session = game.sessions[id]; @@ -770,10 +790,12 @@ const getPrevPlayer = (game, name) => { return name; } -const getValidCorners = (game) => { +const getValidCorners = (game, color) => { const limits = []; /* For each corner, if the corner already has a color set, skip it + * If we are limiting based on active player, a corner is only valid + * if it connects to a road that is owned by that player. * If no color is set, walk each road that leaves that corner and * check to see if there is a settlement placed at the end of that road * If so, this location cannot have a settlement. @@ -782,7 +804,16 @@ const getValidCorners = (game) => { if (game.placements.corners[cornerIndex].color) { return; } - let valid = true; + let valid; + if (!color) { + valid = true; /* Not filtering based on current player */ + } else { + valid = false; + for (let r = 0; !valid && r < corner.roads.length; r++) { + valid = game.placements.roads[corner.roads[r]].color === color; + } + } + for (let r = 0; valid && r < corner.roads.length; r++) { const road = layout.roads[corner.roads[r]]; for (let c = 0; valid && c < road.corners.length; c++) { @@ -791,7 +822,7 @@ const getValidCorners = (game) => { continue; } /* There is a settlement within one segment from this - * corner, so it is invalid for settlement placement */ + * corner, so it is invalid for settlement placement */ if (game.placements.corners[road.corners[c]].color) { valid = false; } @@ -805,6 +836,43 @@ const getValidCorners = (game) => { return limits; } +const getValidRoads = (game, color) => { + const limits = []; + + /* For each road, if the road is set, skip it. + * If no color is set, check the two corners. If the corner + * has a matching color, add this to the set. Otherwise skip. + */ + layout.roads.forEach((road, roadIndex) => { + if (game.placements.roads[roadIndex].color) { + return; + } + let valid = false; + for (let c = 0; !valid && c < road.corners.length; c++) { + const corner = layout.corners[road.corners[c]], + cornerColor = game.placements.corners[road.corners[c]].color; + /* Roads do not pass through other player's settlements */ + if (cornerColor && cornerColor !== color) { + continue; + } + for (let r = 0; !valid && r < corner.roads.length; r++) { + /* This side of the corner is pointing to the road being validated. Skip it. */ + if (corner.roads[r] === roadIndex) { + continue; + } + if (game.placements.roads[corner.roads[r]].color === color) { + valid = true; + } + } + } + if (valid) { + limits.push(roadIndex); + } + }); + + return limits; +} + router.put("/:id/:action/:value?", async (req, res) => { const { action, id } = req.params, value = req.params.value ? req.params.value : ""; @@ -827,7 +895,7 @@ router.put("/:id/:action/:value?", async (req, res) => { return sendGame(req, res, game, error); } - const session = getSession(game, req.session); + const session = getSession(game, req.session), player = session.player; switch (action) { case 'player-name': @@ -881,15 +949,13 @@ router.put("/:id/:action/:value?", async (req, res) => { break; } - if (!error) { - const next = getNextPlayer(game, name); - game.turn = { - name: next, - color: getColorFromName(game, next) - }; - addChatMessage(game, session, `${name} passed their turn.`); - addChatMessage(game, null, `It is ${next}'s turn.`); - } + const next = getNextPlayer(game, name); + game.turn = { + name: next, + color: getColorFromName(game, next) + }; + addChatMessage(game, session, `${name} passed their turn.`); + addChatMessage(game, null, `It is ${next}'s turn.`); break; case 'place-robber': if (game.state !== 'normal' && game.turn.roll !== 7) { @@ -968,6 +1034,37 @@ router.put("/:id/:action/:value?", async (req, res) => { } game.turn.robberDone = true; break; + case 'buy-settlement': + 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 (player.brick < 1 || player.wood < 1 || player.wheat < 1 || player.sheep < 1) { + error = `You have insufficient resources to build a settlement.`; + break; + } + if (player.settlements < 1) { + error = `You have already built all of your settlements.`; + break; + } + let corners = getValidCorners(game, session.color); + if (corners.length === 0) { + error = `There are no valid locations for you to place a settlement.`; + break; + } + player.settlements--; + player.brick--; + player.wood--; + player.wheat--; + player.sheep--; + game.turn.actions = ['place-settlement']; + game.turn.limits = { corners }; + addChatMessage(game, session, `Purchased a settlement. Next, they need to place it.`); + break; case 'place-settlement': if (game.state !== 'initial-placement' && game.state !== 'normal') { error = `You cannot place an item unless the game is active.`; @@ -994,17 +1091,47 @@ router.put("/:id/:action/:value?", async (req, res) => { } corner.color = session.color; corner.type = 'settlement'; - if (game.state === 'initial-placement') { + if (game.state === 'normal') { + game.turn.actions = []; + game.turn.limits = {}; + addChatMessage(game, session, `${name} placed a settlement.`); + } else if (game.state === 'initial-placement') { if (game.direction && game.direction === 'backward') { session.initialSettlement = index; } game.turn.actions = ['place-road']; game.turn.limits = { roads: layout.corners[index].roads }; /* road placement is limited to be near this corner */ addChatMessage(game, session, `Placed a settlement. Next, they need to place a road.`); - } else { - error = `Settlement placement not enabled for normal game play.`; + } + break; + case 'buy-road': + if (game.state !== 'normal') { + error = `You cannot purchase a road 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 (player.brick < 1 || player.wood < 1) { + error = `You have insufficient resources to build a road.`; + break; + } + if (player.roads < 1) { + error = `You have already built all of your roads.`; + break; + } + let roads = getValidRoads(game, session.color); + if (roads.length === 0) { + error = `There are no valid locations for you to place a road.`; + break; + } + player.roads--; + player.brick--; + player.wood--; + game.turn.actions = ['place-road']; + game.turn.limits = { roads }; + addChatMessage(game, session, `Purchased a road. Next, they need to place it.`); break; case 'place-road': if (game.state !== 'initial-placement' && game.state !== 'normal') { @@ -1030,9 +1157,15 @@ router.put("/:id/:action/:value?", async (req, res) => { error = `This location already has a road belonging to ${playerNameFromColor(game, road.color)}!`; break; } - if (game.state === 'initial-placement') { - road.color = session.color; + road.color = session.color; + + if (game.state === 'normal') { + game.turn.actions = []; + game.turn.limits = {}; addChatMessage(game, session, `${name} placed a road.`); + } else if (game.state === 'initial-placement') { + addChatMessage(game, session, `${name} placed a road.`); + let next; if (game.direction === 'forward' && getLastPlayerName(game) === name) { game.direction = 'backward'; @@ -1093,11 +1226,8 @@ router.put("/:id/:action/:value?", async (req, res) => { addChatMessage(game, null, `It is ${name}'s turn.`); game.state = 'normal'; } - } else { - error = `Road placement not enabled for normal game play.`; - break; } - break; + break; case 'place-city': error = `City placement not yet implemented!`; break; @@ -1106,7 +1236,7 @@ router.put("/:id/:action/:value?", async (req, res) => { error = `You can only discard due to the Robber!`; break; } - const discards = req.body, player = session.player; + const discards = req.body; let sum = 0; for (let type in discards) { if (player[type] < parseInt(discards[type])) {