diff --git a/client/src/Board.css b/client/src/Board.css index aff39ba..547531e 100644 --- a/client/src/Board.css +++ b/client/src/Board.css @@ -33,13 +33,15 @@ background-size: 600% auto; /* pip-numbers is a 6x6 grid of pip images */ width: 2em; height: 2em; - transform: translate(-50%, -50%); + transform: translate(-50%, -50%); } .Pip.Active { filter: drop-shadow(0px 0px 5px rgba(255, 255, 0, 0.9)); } +.Pips[disabled], +.Tiles[disabled], .Roads[disabled], .Corners[disabled] { pointer-events: none; @@ -107,19 +109,40 @@ right: 0px; top: 0px; bottom: 0px; - clip-path: circle(50%); + /* clip-path: circle(50%);*/ } +.Tile-Shape { + display: flex; + position: absolute; + left: 0px; + right: 0px; + top: 0px; + bottom: 0px; + clip-path: polygon(25% 0%,75% 0%,100% 50%,75% 100%,25% 100%,0% 50%); +} .Option { + cursor: pointer; pointer-events: all; } +.Option .Pip-Shape, +.Option .Tile-Shape, .Option .Corner-Shape, .Option .Road-Shape { background-color: rgba(255, 255, 255, 0.5); } +.Robber .Pip-Shape, +.Pip-Shape:hover { + clip-path: circle(45%); /* show through the border */ + background-size: contain; + background-position: center; + background-image:url("./assets/woman-robber.png"); +} + +.Tile-Shape:hover, .Corner-Shape:hover, .Road-Shape:hover { background-color: white; diff --git a/client/src/Board.js b/client/src/Board.js index 4e1deb2..e4cfae3 100644 --- a/client/src/Board.js +++ b/client/src/Board.js @@ -47,8 +47,8 @@ const Board = ({ table, game }) => { height: `${tileImageHeight}px`, backgroundImage: `url(${assetsPath}/gfx/tiles-${tile.type}.png)`, backgroundPositionY: `-${tile.card*tileHeight}px` - }} - />; + }} + >
; }; const Road = ({road}) => { @@ -85,6 +85,27 @@ const Board = ({ table, game }) => { >
; }; + const Pip = ({pip}) => { + const onClick = (event) => { + console.log(`Pip ${pip.index}:`, game.layout.corners[pip.index]); + table.placeRobber(pip.index); + return; + }; + + return
; + }; + const generateRoads = () => { let row = 0, rowCount = 0; let y = -2.5 + tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth, @@ -229,20 +250,21 @@ const Board = ({ table, game }) => { let row = 0, rowCount = 0; let y = tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth, x = -(rows[row] - 1) * 0.5 * tileHeight; + let index = 0; + let pip; return game.pipOrder.map(order => { - const pip = game.pips[order]; - const div =
; + />; if (++rowCount === rows[row]) { row++; @@ -389,10 +411,11 @@ const Board = ({ table, game }) => { } }); /* Clear all 'Option' targets */ - const nodes = document.querySelectorAll(`.Option`); + let nodes = document.querySelectorAll(`.Option`); for (let i = 0; i < nodes.length; i++) { nodes[i].classList.remove('Option'); } + /* Add 'Option' based on game.turn.limits */ if (game.turn && game.turn.limits) { if (game.turn.limits['roads']) { @@ -413,6 +436,38 @@ const Board = ({ table, game }) => { el.classList.add('Option'); }); } + if (game.turn.limits['tiles']) { + game.turn.limits['tiles'].forEach(index => { + const el = document.querySelector(`.Tile[data-index="${index}"]`); + if (!el) { + return; + } + el.classList.add('Option'); + }); + } + if (game.turn.limits['pips']) { + game.turn.limits['pips'].forEach(index => { + const el = document.querySelector(`.Pip[data-index="${index}"]`); + if (!el) { + return; + } + el.classList.add('Option'); + }); + } + } + } + + if (game) { + let nodes = document.querySelectorAll(`.Pip .Robber`); + for (let i = 0; i < nodes.length; i++) { + nodes[i].classList.remove('Robber'); + } + + if (game.robber) { + const el = document.querySelector(`.Pip[data-index="${game.robber}"]`); + if (el) { + el.classList.add('Robber'); + } } } @@ -424,9 +479,13 @@ const Board = ({ table, game }) => {
{ borders } - { tiles } - { pips } { game && <> +
+ { tiles } +
+
+ { pips } +
{ corners }
diff --git a/client/src/Table.css b/client/src/Table.css index 0901d1c..287182d 100755 --- a/client/src/Table.css +++ b/client/src/Table.css @@ -306,9 +306,15 @@ } .Hand { + display: flex; min-height: calc(7.2em + 0.5em); } +.Hand > button { + align-self: center; + justify-self: center; +} + .Hand:hover .Stack:hover > *:not(:first-child) { margin-left: -2em; } diff --git a/client/src/Table.js b/client/src/Table.js index 09702fc..94e3b67 100755 --- a/client/src/Table.js +++ b/client/src/Table.js @@ -136,6 +136,7 @@ const Resource = ({ type, count }) => {
{ React.Children.map(array, i => (
@@ -336,6 +337,16 @@ const GameOrder = ({table}) => { }; const Action = ({ table }) => { + const discardClick = (event) => { + const nodes = document.querySelectorAll('.Hand .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")]++; + nodes[i].classList.remove('Selected'); + } + return table.discard(discarding); + } + const newTableClick = (event) => { return table.shuffleTable(); }; @@ -357,7 +368,9 @@ const Action = ({ table }) => { return (); } - const inLobby = table.game.state === 'lobby'; + const inLobby = table.game.state === 'lobby', + player = table.game ? table.game.player : undefined; + return ( { inLobby && <> @@ -368,6 +381,9 @@ const Action = ({ table }) => { + { table.game.turn.roll === 7 && player && player.mustDiscard > 0 && + + } } { !inLobby && @@ -493,6 +509,7 @@ class Table extends React.Component { this.rollDice = this.rollDice.bind(this); this.setGameState = this.setGameState.bind(this); this.shuffleTable = this.shuffleTable.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); @@ -577,6 +594,10 @@ class Table extends React.Component { }); } + discard(resources) { + return this.sendAction('discard', undefined, resources); + } + passTurn() { return this.sendAction('pass'); }; @@ -662,6 +683,10 @@ class Table extends React.Component { }); } + placeRobber(robber) { + return this.sendAction('place-robber', robber); + }; + placeSettlement(settlement) { return this.sendAction('place-settlement', settlement); } @@ -813,6 +838,24 @@ class Table extends React.Component { case '': message = <>{message}The game is in a wonky state. Sorry :(; break; + case 'normal': + if (this.game && this.game.turn) { + if (this.game.turn.roll === 7) { + message = <>{message}Robber was rolled!; + let move = true; + for (let color in this.game.players) { + const discard = this.game.players[color].mustDiscard; + if (discard) { + move = false; + message = <>{message} needs to discard {discard} resources.; + } + } + if (move) { + message = <>{message} {this.game.turn.name} needs to move the robber. + } + } + } + break; default: message = <>{message}Game state is: {this.game.state}; break; @@ -909,15 +952,16 @@ class Table extends React.Component {
- - { player !== undefined &&
- - - - - -
} + + { player !== undefined &&
+ + + + + +
}
+ { game &&
{ this.state.message } {(this.state.pickName || !game.name) && } @@ -934,7 +978,7 @@ class Table extends React.Component { { game && game.turn && game.turn.color !== game.color && (game.state === 'initial-placement' || game.state === 'normal') && - + (!game.player || !game.player.mustDiscard) && } { game && game.showCards && diff --git a/client/src/assets/man-robber.png b/client/src/assets/man-robber.png new file mode 100755 index 0000000..52d2b7d Binary files /dev/null and b/client/src/assets/man-robber.png differ diff --git a/client/src/assets/woman-robber.png b/client/src/assets/woman-robber.png new file mode 100755 index 0000000..007a774 Binary files /dev/null and b/client/src/assets/woman-robber.png differ diff --git a/server/routes/games.js b/server/routes/games.js index 075a573..e539491 100755 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -291,7 +291,11 @@ const distributeResources = (game, roll) => { for (let i = 0; i < game.pipOrder.length; i++) { let index = game.pipOrder[i]; if (assetData.pips[index].roll === roll) { - tiles.push(i); + if (game.robber === i) { + addChatMessage(game, null, `That pesky Robber stole resources!`); + } else { + tiles.push(i); + } } } @@ -350,10 +354,40 @@ const processRoll = (game, dice) => { game.turn.roll = game.dice[0] + game.dice[1]; if (game.turn.roll === 7) { addChatMessage(game, null, `ROBBER! ROBBER! ROBBER!`); + for (let id in game.sessions) { + const player = game.sessions[id].player; + if (player) { + let discard = player.stone + player.wheat + player.brick + player.wood + player.sheep; + if (discard > 7) { + discard = Math.floor(discard / 2); + + player.mustDiscard = discard; + addChatMessage(game, null, `${game.sessions[id].name} must discard ${discard} resource cards.`); + } else { + delete player.mustDiscard; + } + } + } + /* + if you roll a 7, no one receives any resource cards. + instead, every player who has more than 7 resource cards must select + half (rounded down) of their + resource cards and return them to the bank. + then you muyst move the robber: + 1. you must move the robber immediately to the number token of any other + terrain ohex or to the desert hex, + 2. you then steal 1 (random) resourcde card from an opponent who has a settlement or city + adjacent to the target terrain hex. the player who is robbed holds their resource cards + face down. you then take 1 card at random. if the target hex is adjacent to 2 or + more player's settlements or cities, you choose which one you want to steal from. + If the production number for the hex containing the robber is rolled, the owners of + adjacent settlements and citieis do not receive resourcres. The robber prevents it. + */ } else { distributeResources(game, game.turn.roll); } } + const getPlayer = (game, color) => { if (!game) { return { @@ -846,6 +880,34 @@ router.put("/:id/:action/:value?", async (req, res) => { addChatMessage(game, null, `It is ${next}'s turn.`); } break; + case 'place-robber': + if (game.state !== 'normal' && game.turn.roll !== 7) { + error = `You cannot place robber unless 7 was rolled!`; + break; + } + if (game.turn.name !== name) { + error = `You cannot place the robber when it isn't your turn.`; + break; + } + for (let color in game.players) { + if (game.players[color].status === 'Not active') { + continue; + } + if (game.players[color].mustDiscard > 0) { + error = `You cannot place the robber until everyone has discarded!`; + break; + } + } + const robber = parseInt(value); + if (game.robber === robber) { + error = `You must move the robber to a new location!`; + break; + } + game.robber = robber; + game.turn.placedRobber = true; + addChatMessage(game, session, `Robber has been moved!`); + addChatMessage(game, null, 'TODO: Look up which players are on tile, then allow robber-roller to select which player to steal from.'); + break; case 'place-settlement': if (game.state !== 'initial-placement' && game.state !== 'normal') { error = `You cannot place an item unless the game is active.`; @@ -979,6 +1041,33 @@ router.put("/:id/:action/:value?", async (req, res) => { case 'place-city': error = `City placement not yet implemented!`; break; + case 'discard': + if (game.turn.roll !== 7) { + error = `You can only discard due to the Robber!`; + break; + } + const discards = req.body, player = session.player; + let sum = 0; + for (let type in discards) { + if (player[type] < parseInt(discards[type])) { + error = `You have requested to discard more ${type} than you have.` + break; + } + sum += parseInt(discards[type]); + } + if (sum > player.mustDiscard) { + error = `You have requested to discard more cards than you are allowed!`; + break; + } + for (let type in discards) { + player[type] -= parseInt(discards[type]); + player.mustDiscard -= parseInt(discards[type]) + } + addChatMessage(game, null, `${session.name} discarded ${sum} resource cards.`); + if (player.mustDiscard) { + addChatMessage(game, null, `${session.name} must discard ${player.mustDiscard} more cards.`); + } + break; case "state": const state = value; if (!state) { @@ -1048,6 +1137,28 @@ const sendGame = async (req, res, game, error) => { } game.active = active; + /* If the current turn is a robber placement, and everyone has + * discarded, set the limits for where the robber can be placed */ + if (game.turn.roll === 7) { + let move = true; + for (let color in game.players) { + const discard = game.players[color].mustDiscard; + if (discard) { + move = false; + } + } + if (move && !game.turn.placedRobber) { + game.turn.actions = [ 'place-robber' ]; + game.turn.limits = { pips: [] }; + for (let i = 0; i < 19; i++) { + if (i === game.robber) { + continue; + } + game.turn.limits.pips.push(i); + } + } + } + /* Update the session lastActive clock */ let session; if (req.session) { @@ -1117,6 +1228,14 @@ const resetGame = (game) => { roads: [] }; + for (let key in game.players) { + game.players[key].wheat = + game.players[key].sheep = + game.players[key].stone = + game.players[key].brick = + game.players[key].wood = 0; + } + for (let i = 0; i < layout.corners.length; i++) { game.placements.corners[i] = { color: undefined, @@ -1247,6 +1366,7 @@ const shuffleBoard = (game) => { * the target pip value to the currently incremeneting * pip value. */ if (game.tiles[game.tileOrder[target]].type === 'desert') { + game.robber = target; game.pipOrder[target] = 18; } else { game.pipOrder[target] = p++;