diff --git a/client/src/Board.css b/client/src/Board.css index 47717f9..0fead34 100644 --- a/client/src/Board.css +++ b/client/src/Board.css @@ -37,11 +37,11 @@ } .Pip.Active { - filter: drop-shadow(0px 0px 5px rgba(255, 255, 0, 0.9)); + filter: drop-shadow(0px 0px 10px rgba(255, 0, 255, 1)); } .Pip.Active.Option { - filter: brightness(150%) drop-shadow(0px 0px 5px rgba(255, 255, 0, 0.9)); + filter: brightness(150%) drop-shadow(0px 0px 10px rgba(255, 0, 255, 1)); } .Pips[disabled], diff --git a/client/src/Table.js b/client/src/Table.js index 9339775..c011f14 100755 --- a/client/src/Table.js +++ b/client/src/Table.js @@ -48,8 +48,10 @@ const Placard = ({table, type, active}) => { } const buildClicked = (event) => { - if (!table.state.buildActive) { - table.setState({ buildActive: true }); + if (!type.match(/^l.*/)) { + if (!table.state.buildActive) { + table.setState({ buildActive: true }); + } } }; @@ -1004,6 +1006,13 @@ class Table extends React.Component {
{ development }
+ { game.longestRoad && game.longestRoad === game.color && + + } } - { game && game.showCards && -
- { game && game.state === "active" && <> -
In hand
- -
Available to play
-
- - -
- - -
-
-
Points
-
-
- - - -
-
-
- - -
- } -
- } - { this.state.error &&
{this.state.error}
} diff --git a/server/routes/games.js b/server/routes/games.js index 3f16346..ee42980 100755 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -805,6 +805,185 @@ const getPrevPlayer = (game, name) => { return name; } +const processCorner = (game, color, cornerIndex, placedCorner) => { + /* If this corner is allocated and isn't assigned to the walking color, skip it */ + if (placedCorner.color && placedCorner.color !== color) { + return -1; + } + /* If this corner is already being walked, skip it */ + if (placedCorner.walking) { + return -1; + } + + placedCorner.walking = true; + /* Calculate the longest road branching from both corners */ + let longest = 0; + layout.corners[cornerIndex].roads.forEach(roadIndex => { + const placedRoad = game.placements.roads[roadIndex]; + longest = Math.max(processRoad(game, color, roadIndex, placedRoad), longest); + }); + + return longest; +}; + +const buildCornerGraph = (game, color, cornerIndex, placedCorner, set) => { + /* If this corner is allocated and isn't assigned to the walking color, skip it */ + if (placedCorner.color && placedCorner.color !== color) { + return; + } + /* If this corner is already being walked, skip it */ + if (placedCorner.walking) { + return; + } + + placedCorner.walking = true; + /* Calculate the longest road branching from both corners */ + layout.corners[cornerIndex].roads.forEach(roadIndex => { + const placedRoad = game.placements.roads[roadIndex]; + buildRoadGraph(game, color, roadIndex, placedRoad, set); + }); +}; + +const processRoad = (game, color, roadIndex, placedRoad) => { + /* If this road isn't assigned to the walking color, skip it */ + if (placedRoad.color !== color) { + return -1; + } + + /* If this road is already being walked, skip it */ + if (placedRoad.walking) { + return -1; + } + + placedRoad.walking = true; + /* Calculate the longest road branching from both corners */ + let longest = 0; + layout.roads[roadIndex].corners.forEach(cornerIndex => { + const placedCorner = game.placements.corners[cornerIndex]; + longest = Math.max(processCorner(game, color, cornerIndex, placedCorner), longest); + }); + placedRoad.longest = 1 + longest; + + return placedRoad.longest; +}; + +const buildRoadGraph = (game, color, roadIndex, placedRoad, set) => { + /* If this road isn't assigned to the walking color, skip it */ + if (placedRoad.color !== color) { + return; + } + /* If this road is already being walked, skip it */ + if (placedRoad.walking) { + return; + } + + placedRoad.walking = true; + set.push(roadIndex); + /* Calculate the longest road branching from both corners */ + layout.roads[roadIndex].corners.forEach(cornerIndex => { + const placedCorner = game.placements.corners[cornerIndex]; + buildCornerGraph(game, color, cornerIndex, placedCorner, set) + }); +}; + +const clearRoadMarkers = (game) => { + /* Clear out walk markers on roads */ + layout.roads.forEach((item, itemIndex) => { + const placed = game.placements.roads[itemIndex]; + placed.walking = false; + placed.longest = 0; + }); + + /* Clear out walk markers on corners */ + layout.corners.forEach((item, itemIndex) => { + const placed = game.placements.corners[itemIndex]; + placed.walking = false; + }); +} + +const calculateRoadLengths = (game, session) => { + clearRoadMarkers(game); + + /* Clear out player longest road counts */ + for (let key in game.players) { + game.players[key].roadLength = 0; + } + + /* Build a set of connected road graphs. Once all graphs are + * constructed, walk through each graph, starting from each + * location in the graph. If the length ever equals the + * number of items in the graph, short circuit--longest path. + * Otherwise, check all paths from each segment. This is + * needed to catch loops where starting from an outside end + * point may result in not counting the length of the loop + */ + let graphs = []; + layout.roads.forEach((road, roadIndex) => { + const placedRoad = game.placements.roads[roadIndex]; + if (placedRoad.color) { + let set = []; + buildRoadGraph(game, placedRoad.color, roadIndex, placedRoad, set); + if (set.length) { + graphs.push({ color: placedRoad.color, set }); + } + } + }); + + console.log(graphs); + + let currentLength = game.longestRoad ? game.players[game.longestRoad].roadLength : -1, + currentLongest = game.longestRoad; + clearRoadMarkers(game); + graphs.forEach(graph => { + graph.set.forEach(roadIndex => { + const placedRoad = game.placements.roads[roadIndex]; + clearRoadMarkers(game); + const length = processRoad(game, placedRoad.color, roadIndex, placedRoad); + game.players[placedRoad.color].roadLength = + Math.max(game.players[placedRoad.color].roadLength, length); + }); + }); + + const checkForTies = false; + if (currentLongest && game.players[game.currentLongest].roadLength < currentLength) { + addChatMessage(game, session, `${getPlayerNameFromColor(game, game.currentLongest)} had their longest road split!`); + checkForTies = true; + } + + let longest = game.longestRoad ? game.players[game.longestRoad].roadLength : 4, + longestPlayers = []; + for (let key in game.players) { + if (game.players[key].status === 'Not active') { + continue; + } + if (game.players[key].roadLength > longest) { + longestPlayers = [ key ]; + longest = game.players[key].roadLength; + } else if (game.players[key].roadLength == longest && checkForTies) { + longestPlayers.push(key); + } + } + + if (longestPlayers.length > 0) { + if (longestPlayers.length === 1) { + if (game.longestRoad !== longestPlayers[0]) { + game.longestRoad = longestPlayers[0]; + addChatMessage(game, session, `${playerNameFromColor(game, game.longestRoad)} now has the longest road (${longest})!`); + } + } else { + if (checkForTies) { + const names = longestPlayers.map(color => playerNameFromColor(color)); + addChatMessage(game, session, `${names.join(', ')} are tied for longest road (${longest})!`); + } + game.longestRoad = null; + } + } else { + game.longestRoad = null; + } + + +}; + const getValidCorners = (game, color, type) => { const limits = []; @@ -982,6 +1161,7 @@ router.put("/:id/:action/:value?", async (req, res) => { /* Any player can make an offer */ if (value === 'offer') { const offer = req.body; + console.log('TODO: Verify player has sufficient resources.'); session.player.gives = offer.gives; session.player.gets = offer.gets; if (game.turn.name === name) { @@ -1006,7 +1186,7 @@ router.put("/:id/:action/:value?", async (req, res) => { target = game.players[offer.color]; offer.gives.forEach(item => { const isOffered = target.gives.find( - match => match.type === item.type && match.count === item.count); + match => match.type === item.type && match.count === item.count); if (!isOffered) { mismatch = true; } @@ -1022,12 +1202,19 @@ router.put("/:id/:action/:value?", async (req, res) => { error = `Unfortunately, trades were re-negotiated in transit and the deal is invalid!`; break; } - } + } + /* Verify the requesting offer wasn't jacked */ + console.log('TODO: Verify the player trade matches the offer target'); + /* Transfer goods */ player.gets.forEach(item => { if (target) { target[item.type] -= item.count; + if (target[item.type] < 0) { + console.log(`Cheating!!!`); + target[item.type] = 0; + } } player[item.type] += item.count; }); @@ -1036,6 +1223,10 @@ router.put("/:id/:action/:value?", async (req, res) => { target[item.type] += item.count; } player[item.type] -= item.count; + if (player[item.type] < 0) { + console.log(`Cheating!!!`); + player[item.type] = 0; + } }); delete game.turn.offer; @@ -1162,6 +1353,8 @@ router.put("/:id/:action/:value?", async (req, res) => { }); if (cards.length === 0) { addChatMessage(game, session, `Victim did not have any cards to steal.`); + game.turn.actions = []; + game.turn.limits = {}; } else { let index = Math.floor(Math.random() * cards.length), type = cards[index]; @@ -1299,6 +1492,7 @@ router.put("/:id/:action/:value?", async (req, res) => { game.turn.actions = []; game.turn.limits = {}; addChatMessage(game, session, `${name} placed a settlement.`); + calculateRoadLengths(game, session); } else if (game.state === 'initial-placement') { if (game.direction && game.direction === 'backward') { session.initialSettlement = index; @@ -1340,7 +1534,7 @@ router.put("/:id/:action/:value?", async (req, res) => { error = `You cannot build until you have rolled.`; break; } - if (player.wheat < 3 || player.stone < 2) { + if (player.wheat < 2 || player.stone < 3) { error = `You have insufficient resources to build a city.`; break; } @@ -1386,7 +1580,7 @@ router.put("/:id/:action/:value?", async (req, res) => { error = `This location already has a city!`; break; } - if (player.wheat < 3 || player.stone < 2) { + if (player.wheat < 2 || player.stone < 3) { error = `You have insufficient resources to build a city.`; break; } @@ -1398,8 +1592,8 @@ router.put("/:id/:action/:value?", async (req, res) => { corner.type = 'city'; player.cities--; player.settlements++; - player.wheat -= 3; - player.stone -= 2; + player.wheat -= 2; + player.stone -= 3; game.turn.actions = []; game.turn.limits = {}; addChatMessage(game, session, `${name} upgraded a settlement to a city!`); @@ -1477,9 +1671,12 @@ router.put("/:id/:action/:value?", async (req, res) => { game.turn.actions = []; game.turn.limits = {}; addChatMessage(game, session, `${name} placed a road.`); + calculateRoadLengths(game, session); + } else if (game.state === 'initial-placement') { road.color = session.color; addChatMessage(game, session, `${name} placed a road.`); + calculateRoadLengths(game, session); let next; if (game.direction === 'forward' && getLastPlayerName(game) === name) { @@ -1502,6 +1699,7 @@ router.put("/:id/:action/:value?", async (req, res) => { name: next, color: getColorFromName(game, next) }; + calculateRoadLengths(game, session); addChatMessage(game, null, `It is ${next}'s turn. Place a settlement.`); } else { game.turn = { @@ -1737,13 +1935,34 @@ const resetGame = (game) => { roads: [] }; + Object.assign(game, { + sheep: 19, + ore: 19, + wool: 19, + brick: 19, + wheat: 19, + longestRoad: null, + largestArmy: null, + developmentCards: assetData.developmentCards.slice() + }); + 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; + Object.assign(game.players[key], { + wheat: 0, + sheep: 0, + stone: 0, + brick: 0, + wood: 0, + roads: 15, + cities: 4, + settlements: 5, + points: 0, + development: [] + }); } + + game.developmentCards = assetData.developmentCards.slice(); + shuffle(game.developmentCards); for (let i = 0; i < layout.corners.length; i++) { game.placements.corners[i] = { @@ -1883,7 +2102,7 @@ const shuffleBoard = (game) => { } } - shuffle(game.developmentCards); + shuffle(game.developmentCards); } /*