diff --git a/client/src/Activities.js b/client/src/Activities.js
index 5843ff4..77aaf26 100644
--- a/client/src/Activities.js
+++ b/client/src/Activities.js
@@ -5,7 +5,7 @@ import { PlayerColor } from './PlayerColor.js';
import { Dice } from './Dice.js';
import { GlobalContext } from "./GlobalContext.js";
-const Activity = ({ activity }) => {
+const Activity = ({ keep, activity }) => {
const [animation, setAnimation] = useState('open');
const [display, setDisplay] = useState(true)
@@ -16,7 +16,7 @@ const Activity = ({ activity }) => {
setDisplay(false)
};
- if (display) {
+ if (display && !keep) {
setTimeout(() => { hide(10000) }, 0);
}
@@ -157,10 +157,11 @@ const Activities = () => {
discarders.push(
{name} must discard {player.mustDiscard} cards.
);
}
- const list = activities
- .filter(activity => timestamp - activity.date < 11000)
- .map(activity => {
- return ;
+ let list = activities
+ .filter((activity, index) =>
+ activities.length - 1 === index || timestamp - activity.date < 11000);
+ list = list.map((activity, index) => {
+ return ;
});
let who;
diff --git a/server/ai/app.js b/server/ai/app.js
index 0979bbc..67d04b1 100644
--- a/server/ai/app.js
+++ b/server/ai/app.js
@@ -1,6 +1,10 @@
const fetch = require('node-fetch');
const WebSocket = require('ws');
const fs = require('fs').promises;
+const calculateLongestRoad = require('./longest-road.js');
+
+const { getValidRoads, getValidCorners } = require('../util/validLocations.js');
+const layout = require('../util/layout.js');
const version = '0.0.1';
@@ -18,16 +22,28 @@ For example:
const server = process.argv[2];
const gameId = process.argv[3];
-let session = undefined;
const name = process.argv[4];
const game = {};
+const anyValue = undefined;
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
+
+/* Do not use arrow function as this is rebound to have
+ * this as the WebSocket */
+let send = function (data) {
+ if (data.type === 'get') {
+ console.log(`ws - send: get`, data.fields);
+ } else {
+ console.log(`ws - send: ${data.type}`);
+ }
+ this.send(JSON.stringify(data));
+};
+
const error = (e) => {
console.log(`ws - error`, e);
-}
+};
const connect = async () => {
let loc = new URL(server), new_uri;
@@ -69,6 +85,8 @@ const connect = async () => {
'Cookie': `player=${player}`
}
});
+ send = send.bind(ws);
+
return new Promise((resolve, reject) => {
const headers = (e) => {
console.log(`ws - headers`);
@@ -92,27 +110,182 @@ const connect = async () => {
ws.on('headers', headers);
ws.on('close', close);
ws.on('error', error);
- ws.on('message', async (data) => { await message(ws, data); });
+ ws.on('message', async (data) => { await message(data); });
});
};
-const createPlayer = (ws) => {
- const send = (data) => {
- ws.send(JSON.stringify(data));
- };
+const createPlayer = () => {
+};
- if (game.name === '') {
- send({ type: 'player-name', name });
- return;
- }
+const types = [ 'wheat', 'brick', 'stone', 'sheep', 'wood' ];
- if (game.state !== 'lobby') {
- return;
+const tryBuild = () => {
+ let waitingFor = undefined;
+
+ if (!waitingFor
+ && game.private.settlements
+ && game.private.wood
+ && game.private.brick
+ && game.private.sheep
+ && game.private.wheat) {
+ const corners = getValidCorners(game, game.color);
+ if (corners.length) {
+ send({
+ type: 'buy-settlement'
+ });
+ waitingFor = {
+ turn: {
+ actions: anyValue
+ }
+ };
+ }
}
- if (game.unselected.indexOf(name) === -1) {
+ if (!waitingFor
+ && game.private.cities
+ && game.private.stone >= 3
+ && game.private.wheat >= 2) {
+ const corners = getValidCorners(game, game.color, 'settlement');
+ if (corners.length) {
+ send({
+ type: 'buy-city'
+ });
+ waitingFor = {
+ turn: {
+ actions: anyValue
+ }
+ };
+ }
+ }
+
+ if (!waitingFor
+ && game.private.roads
+ && game.private.wood
+ && game.private.brick) {
+ const roads = getValidRoads(game, game.color);
+ if (roads.length) {
+ send({
+ type: 'buy-road'
+ });
+ waitingFor = {
+ turn: {
+ actions: anyValue
+ }
+ };
+ }
+ }
+
+ if (!waitingFor
+ && game.private.wheat
+ && game.private.stone
+ && game.private.sheep) {
+ send({
+ type: 'buy-development'
+ });
+ waitingFor = {
+ private: {
+ development: anyValue
+ }
+ };
+ }
+
+ return waitingFor;
+};
+
+let sleeping = false;
+const sleep = async (delay) => {
+ if (sleeping) {
return;
}
+ sleeping = true;
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ sleeping = false;
+ resolve();
+ }, delay);
+ });
+};
+
+const bestRoadPlacement = (game) => {
+ const road = calculateLongestRoad(game);
+ console.log(`${name} - could make road ${road.segments + 1} long on ${road.index}`);
+
+ let attempt = -1;
+ layout.roads[road.index].corners.forEach(cornerIndex => {
+ if (attempt !== -1) {
+ return;
+ }
+ layout.corners[cornerIndex].roads.forEach(roadIndex => {
+ if (attempt !== -1) {
+ return;
+ }
+ const placedRoad = game.placements.roads[roadIndex];
+ if (placedRoad.color) {
+ return;
+ }
+ attempt = roadIndex;
+ });
+ });
+
+ if (game.turn.limits.roads.indexOf(attempt) !== -1) {
+ console.log(`${name} - attempting to place on end of longest road`);
+ return attempt;
+ } else {
+ console.log(`${name} - selecting a random road location`);
+ return game.turn.limits.roads[Math.floor(
+ Math.random() * game.turn.limits.roads.length)];
+ }
+}
+
+const isMatch = (input, received) => {
+ for (let key in input) {
+ /* received update didn't contain this field */
+ if (!(key in received)) {
+ return false;
+ }
+ /* Received object had a value we were waiting to have set */
+ if (input[key] === anyValue && (key in received)) {
+ continue;
+ }
+ /* waitingFor field is an object, so recurse */
+ if (typeof input[key] === 'object') {
+ if (!isMatch(input[key], received[key])) {
+ return false
+ }
+ /* object matched; go to next field */
+ continue;
+ }
+ /* No match in requested key... */
+ if (input[key] !== received[key]) {
+ return false;
+ }
+ /* Value matches */
+ }
+ /* All fields set or matched */
+ return true;
+};
+
+const processLobby = (received) => {
+ if (game.name === '' && !received.name) {
+ send({ type: 'player-name', name });
+ /* Wait for the game.name to be set to 'name' and for unselected */
+ return { name, players: anyValue, unselected: anyValue };
+ }
+
+ if (!received.unselected) {
+ return {
+ unselected: anyValue
+ };
+ }
+
+ /* AI selected a Player. Wait for game-order */
+ if (received.unselected.indexOf(name) === -1) {
+ send({
+ type: 'chat',
+ message: `Woohoo! Robot AI ${version} is alive and playing as ${game.color}!`
+ });
+ return { state: 'game-order' };
+ }
const slots = [];
for (let color in game.players) {
@@ -120,11 +293,16 @@ const createPlayer = (ws) => {
slots.push(color);
}
}
+
if (slots.length === 0) {
- return;
+ send({
+ chat: `There are no slots for me to play :(. Waiting for one to open up.`
+ });
+ return { unselected: anyValue };
}
+
const index = Math.floor(Math.random() * slots.length);
- console.log(`Requesting to play as ${slots[index]}.`);
+ console.log(`${name} - requesting to play as ${slots[index]}.`);
game.unselected = game.unselected.filter(
color => color === slots[index]);
send({
@@ -132,248 +310,502 @@ const createPlayer = (ws) => {
field: 'color',
value: slots[index]
});
- send({
- type: 'chat',
- message: `Woohoo! Robot AI ${version} is alive!`
- });
+ return { color: slots[index], state: 'game-order' };
};
-const tryBuild = (ws) => {
- const send = (data) => {
- console.log(`ws - send`);
- ws.send(JSON.stringify(data));
+const processGameOrder = async () => {
+ if (!game.color) {
+ console.log(`game-order - player not active`);
+ return { color };
+ }
+ console.log(`game-order - `, {
+ color: game.color,
+ players: game.players
+ });
+ if (!game.players[game.color].orderRoll || game.players[game.color].tied) {
+ console.log(`Time to roll as ${game.color}`);
+ send({ type: 'roll' });
+ }
+
+ return { turn: { color: game.color }};
+};
+
+const processInitialPlacement = async (received) => {
+ if (!game.turn || game.turn.color !== game.color) {
+ return {
+ turn: {
+ color: game.color,
+ }
+ }
};
- let trying = false;
- if (game.private.settlements
- && game.private.wood
- && game.private.brick
- && game.private.sheep
- && game.private.wheat) {
- send({
- type: 'buy-settlement'
+
+ if (!game.placements) {
+ return {
+ turn: {
+ color: game.color,
+ },
+ placements: anyValue
+ };
+ }
+
+ if (!game.turn.actions) {
+ return {
+ turn: {
+ color: game.color,
+ actions: anyValue
+ },
+ placements: anyValue
+ };
+ }
+
+ let index;
+ const type = game.turn.actions[0];
+ if (type === 'place-road') {
+ index = bestRoadPlacement(game);
+ } else if (type === 'place-settlement') {
+ index = game.turn.limits.corners[Math.floor(
+ Math.random() * game.turn.limits.corners.length)];
+ }
+ console.log(`Selecting ${type} at ${index}`);
+ send({
+ type, index
+ });
+ /* Wait for this player's turn again */
+ return { turn: { color: game.color } };
+}
+
+/* Start watching for a name entry */
+let waitingFor = { name: anyValue }, received = {};
+
+const reducedGame = (game) => {
+ const filters = [ 'chat', 'activities', 'placements', 'players', 'private', 'dice' ];
+ const value = {};
+ for (let key in game) {
+ if (filters.indexOf(key) === -1) {
+ value[key] = game[key];
+ } else {
+ if (Array.isArray(game[key])) {
+ value[key] = `length(${game[key].length})`;
+ } else {
+ value[key] = `...filtered`;
+ }
+ }
+ }
+ return value;
+}
+
+const processWaitingFor = (waitingFor) => {
+ const value = {
+ type: 'get',
+ fields: []
+ };
+ for (let key in waitingFor) {
+ value.fields.push(key);
+ }
+ send(value);
+ received = {};
+}
+
+const processDiscard = async (received) => {
+ if (!game.players) {
+ waitingFor = {
+ players: {}
+ };
+ waitingFor.players[game.color] = undefined;
+ return waitingFor;
+ }
+
+ let mustDiscard = game.players[game.color].mustDiscard;
+
+ if (!mustDiscard) {
+ return;
+ }
+
+ const cards = [],
+ discards = {};
+ types.forEach(type => {
+ for (let i = 0; i < game.private[type]; i++) {
+ cards.push(type);
+ }
+ });
+ while (mustDiscard--) {
+ const type = cards[Math.floor(Math.random() * cards.length)];
+ if (!(type in discards)) {
+ discards[type] = 1;
+ } else {
+ discards[type]++;
+ }
+ }
+ console.log(`discarding - `, discards);
+ send({
+ type: 'discard',
+ discards
+ });
+ waitingFor = {
+ turn: anyValue,
+ players: {}
+ }
+ waitingFor.players[game.color] = anyValue;
+ return waitingFor;
+};
+
+const processTrade = async (received) => {
+ const enough = [];
+ let shouldTrade = true;
+
+ /* Check and see which resources we have enough of */
+ types.forEach(type => {
+ if (game.private[type] >= 4) {
+ enough.push(type);
+ }
+ });
+ shouldTrade = enough.length > 0;
+
+ let least = { type: undefined, count: 0 };
+
+ if (shouldTrade) {
+ /* Find out which resource we have the least amount of */
+ types.forEach(type => {
+ if (game.private[type] <= least.count) {
+ least.type = type;
+ least.count = game.private[type];
+ }
});
- trying = true;
+ if (least.count >= 4) {
+ shouldTrade = false;
+ }
+ }
+
+ /* If trade not active, see if it should be... */
+ if (shouldTrade
+ && (!received.turn.actions
+ || received.turn.actions.indexOf('trade') === -1)) {
+ /* Request trade mode, and wait for it... */
+ console.log(`${name} - starting trade negotiations`);
+ send({
+ type: 'trade'
+ });
+ return {
+ turn: { actions: anyValue }
+ }
}
- if (game.private.wood && game.private.brick && game.private.roads) {
+ /* If we do not have enough resources, and trade is active, cancel */
+ if (!shouldTrade
+ && received.turn.actions
+ && received.turn.actions.indexOf('trade') !== -1) {
+ console.log(`${name} - cancelling trade negotiations`);
send({
- type: 'buy-road'
+ type: 'trade',
+ action: 'cancel'
});
- trying = true;
+ return {
+ turn: anyValue
+ };
}
- return trying;
-};
+ if (!shouldTrade) {
+ return;
+ }
-
-const sleep = async (delay) => {
- return new Promise((resolve) => {
- setTimeout(resolve, delay);
- });
-};
-
-const message = async (ws, data) => {
- const send = (data) => {
- console.log(`ws - send: ${data.type}`);
- ws.send(JSON.stringify(data));
+ const give = {
+ type: enough[Math.floor(Math.random() * enough.length)],
+ count: 4
+ }, get = {
+ type: least.type,
+ count: 1
+ };
+ const offer = {
+ gives: [give],
+ gets: [get]
};
- data = JSON.parse(data);
- switch (data.type) {
- case 'game-update':
-
- Object.assign(game, data.update);
- delete data.update.chat;
- delete data.update.activities;
- console.log(`ws - receive - `,
- data.update
- );
+ if (received.turn.offer) {
+ send({
+ type: 'trade',
+ action: 'accept',
+ offer: {
+ name: 'The bank',
+ gets: [{ type: get.type, count: 1 }],
+ gives: [{ type: give.type, count: give.count }]
+ }
+ });
+ return {
+ turn: {
+ actions: anyValue
+ }
+ };
+ }
- console.log(`state - ${game.state}`);
+ /* Initiate offer... */
+
+ if (!received.turn.offer) {
+ console.log(`trade - `, offer);
+ send({
+ type: 'trade',
+ action: 'offer',
+ offer
+ });
+
+ return {
+ private: { offerRejected: anyValue }
+ };
+ }
+
+ return {
+ turn: anyValue
+ };
+}
+
+const processNormal = async (received) => {
+ let waitingFor = undefined;
+
+ if (!game.turn || !game.private) {
+ return {
+ turn: anyValue,
+ private: anyValue
+ }
+ };
+
+ /* Process things that happen on everyone's turn */
+ waitingFor = await processDiscard(received);
+ if (waitingFor) {
+ return waitingFor;
+ }
+
+ /* From here on it is only actions that occur on the player's turn */
+ if (!received.turn || received.turn.color !== game.color) {
+ console.log(`${name} - waiting for turn... ${game.players[game.turn.color].name} is active.`);
+ console.log({
+ wheat: game.private.wheat,
+ sheep: game.private.sheep,
+ stone: game.private.stone,
+ brick: game.private.brick,
+ wood: game.private.wood,
+ });
+ return {
+ turn: { color: game.color },
+ dice: anyValue
+ };
+ }
+
+ console.log(`${name}'s turn. Processing...`);
+
+ if (!game.dice) {
+ console.log(`${name} - rolling...`);
+ send({
+ type: 'roll'
+ });
+ return {
+ turn: {
+ color: game.color
+ },
+ dice: anyValue
+ };
+ }
+
+ if (received.turn.actions && received.turn.actions.indexOf('place-road') !== -1) {
+ index = bestRoadPlacement(game);
+ send({
+ type: 'place-road', index
+ });
+ return {
+ turn: { color: game.color }
+ };
+ }
+
+ if (game.turn.actions && game.turn.actions.indexOf('place-city') !== -1) {
+ index = game.turn.limits.corners[Math.floor(
+ Math.random() * game.turn.limits.corners.length)];
+ send({
+ type: 'place-city', index
+ });
+ return {
+ turn: { color: game.color }
+ };
+ }
+
+ if (game.turn.actions && game.turn.actions.indexOf('place-settlement') !== -1) {
+ console.log({ corners: game.turn.limits.corners });
+ index = game.turn.limits.corners[Math.floor(
+ Math.random() * game.turn.limits.corners.length)];
+ send({
+ type: 'place-settlement', index
+ });
+ return {
+ turn: { color: game.color }
+ };
+ }
+
+ if (game.turn.actions
+ && game.turn.actions.indexOf('place-robber') !== -1) {
+ console.log({ pips: game.turn.limits.pips });
+ const index = game.turn.limits.pips[Math.floor(Math.random() * game.turn.limits.pips.length)];
+ console.log(`placing robber - ${index}`)
+ send({
+ type: 'place-robber',
+ index
+ });
+ return {
+ turn: { color: game.color }
+ };
+ }
+
+ if (game.turn.actions && game.turn.actions.indexOf('steal-resource') !== -1) {
+ if (!game.turn.limits.players) {
+ console.warn(`No players in limits with steal-resource`);
+ return;
+ }
+ const { color } = game.turn.limits.players[Math.floor(Math.random() * game.turn.limits.players.length)];
+ console.log(`stealing resouce from ${game.players[color].name}`);
+ send({
+ type: 'steal-resource',
+ color
+ });
+ return;
+ }
+
+ if (game.turn.robberInAction) {
+ console.log({ turn: game.turn });
+ return;
+ }
+
+ console.log({
+ wheat: game.private.wheat,
+ sheep: game.private.sheep,
+ stone: game.private.stone,
+ brick: game.private.brick,
+ wood: game.private.wood,
+ });
+
+ waitingFor = await tryBuild(received);
+ if (waitingFor) {
+ return waitingFor;
+ }
+
+ waitingFor = await processTrade(received);
+ if (waitingFor) {
+ return waitingFor;
+ }
+
+ console.log(`${name} - passing`);
+ send({
+ type: 'pass'
+ });
+
+ return { turn: anyValue };
+};
+
+const message = async (data) => {
+ try {
+ data = JSON.parse(data);
+ } catch (error) {
+ console.error(error);
+ console.log(data);
+ return;
+ }
+
+ switch (data.type) {
+ case 'warning':
+ if (game.turn.color === game.color && game.state !== 'lobby') {
+ console.log(`WARNING: ${data.warning}. Passing.`);
+ send({
+ type: 'pass'
+ });
+ waitingFor = {
+ turn: { color: game.color }
+ };
+ processWaitingFor(waitingFor);
+ }
+ break;
+
+ case 'game-update':
+ /* Keep game updated with the latest information */
+ Object.assign(game, data.update);
+ if (sleeping) {
+ if (waitingFor) {
+ Object.assign(received, data.update);
+ }
+ console.log(`${name} - sleeping`);
+ return;
+ }
+
+ if (waitingFor) {
+ Object.assign(received, data.update);
+ if (!isMatch(waitingFor, received)) {
+ console.log(`${name} - still waiting - waitingFor: `,
+ waitingFor);
+ if (game.robberInAction) {
+ console.log(`${name} - robber in action! Must check discards...`);
+ } else {
+ return;
+ }
+ } else {
+ console.log(`${name} - received match - received: `,
+ reducedGame(received));
+ console.log(`${name} - going to sleep`);
+ await sleep(1000 + Math.random() * 500);
+ console.log(`${name} - waking up`);
+ waitingFor = undefined;
+ }
+ }
switch (game.state) {
case undefined:
case 'lobby':
- createPlayer(ws);
- break;
+ waitingFor = await processLobby(received);
+ if (waitingFor) {
+ processWaitingFor(waitingFor);
+ }
+ return;
case 'game-order':
- if (!game.color) {
- console.log(`game-order - player not active`);
- return;
+ waitingFor = await processGameOrder(received);
+ if (waitingFor) {
+ processWaitingFor(waitingFor);
}
- console.log(`game-order - `, {
- color: game.color,
- players: game.players
- });
- if (!game.players[game.color].orderRoll || game.players[game.color].tied) {
- console.log(`Time to roll as ${game.color}`);
- send({ type: 'roll' });
- }
- break;
+ return;
- case 'initial-placement': {
- await sleep(1000 + Math.random() * 500);
- console.log({ color: game.color, state: game.state, turn: game.turn });
- if (game.turn.color !== game.color) {
- break;
+ case 'initial-placement':
+ waitingFor = await processInitialPlacement(received);
+ if (waitingFor) {
+ processWaitingFor(waitingFor);
}
-
- let index;
- const type = game.turn.actions[0];
- if (type === 'place-road') {
- console.log({ roads: game.turn.limits.roads });
- index = game.turn.limits.roads[Math.floor(
- Math.random() * game.turn.limits.roads.length)];
- } else if (type === 'place-settlement') {
- console.log({ corners: game.turn.limits.corners });
- index = game.turn.limits.corners[Math.floor(
- Math.random() * game.turn.limits.corners.length)];
- }
- console.log(`Selecting ${type} at ${index}`);
- send({
- type, index
- });
- } break;
-
+ return;
+
case 'normal':
- if (game.players[game.color].mustDiscard) {
- await sleep(1000 + Math.random() * 500);
- let mustDiscard = game.players[game.color].mustDiscard;
- if (!mustDiscard) {
- return;
- }
- const cards = [],
- discards = {};
- const types = ['wheat', 'sheep', 'stone', 'brick', 'wood'];
- types.forEach(type => {
- for (let i = 0; i < game.private[type]; i++) {
- cards.push(type);
- }
- });
- while (mustDiscard--) {
- const type = cards[Math.floor(Math.random() * cards.length)];
- if (!(type in discards)) {
- discards[type] = 1;
- } else {
- discards[type]++;
- }
- }
- console.log(`discarding - `, discards);
- send({
- type: 'discard',
- discards
- });
- return;
+ waitingFor = await processNormal(received);
+ if (waitingFor) {
+ processWaitingFor(waitingFor);
}
-
- if (game.turn.color !== game.color) {
- console.log(`not ${name}'s turn.`)
- return;
- }
-
- await sleep(1000 + Math.random() * 500);
- if (game.turn.color !== game.color) {
- return;
- }
-
- if (game.turn.actions && game.turn.actions.indexOf('place-road') !== -1) {
- index = game.turn.limits.roads[Math.floor(
- Math.random() * game.turn.limits.roads.length)];
- send({
- type: 'place-road', index
- });
- return;
- }
-
- if (game.turn.actions && game.turn.actions.indexOf('place-settlement') !== -1) {
- console.log({ corners: game.turn.limits.corners });
- index = game.turn.limits.corners[Math.floor(
- Math.random() * game.turn.limits.corners.length)];
- send({
- type: 'place-settlement', index
- });
- return;
- }
-
- if (!game.dice) {
- console.log(`Rolling...`);
- send({
- type: 'roll'
- });
- return;
- }
-
- if (game.turn.actions
- && game.turn.actions.indexOf('place-robber') !== -1) {
- console.log({ pips: game.turn.limits.pips });
- const index = game.turn.limits.pips[Math.floor(Math.random() * game.turn.limits.pips.length)];
- console.log(`placing robber - ${index}`)
- send({
- type: 'place-robber',
- index
- });
- return;
- }
-
- if (game.turn.actions && game.turn.actions.indexOf('steal-resource') !== -1) {
- if (!game.turn.limits.players) {
- console.warn(`No players in limits with steal-resource`);
- return;
- }
- const { color } = game.turn.limits.players[Math.floor(Math.random() * game.turn.limits.players.length)];
- console.log(`stealing resouce from ${game.players[color].name}`);
- send({
- type: 'steal-resource',
- color
- });
- return;
- }
-
- if (game.turn.robberInAction) {
- console.log({ turn: game.turn });
- } else {
- console.log({
- turn: game.turn,
- wheat: game.private.wheat,
- sheep: game.private.sheep,
- stone: game.private.stone,
- brick: game.private.brick,
- wood: game.private.wood,
- });
- if (!tryBuild(ws)) {
- send({
- type: 'pass'
- });
- }
- }
- break;
- default:
- console.log({ state: game.state, turn: game.turn });
- break;
- }
- break;
-
- case 'ping':
- if (!game.state) {
- console.log(`ping received with no game. Sending update request`);
- ws.send(JSON.stringify({
- type: 'game-update'
- }));
+ return;
}
break;
+ case 'ping':
+ if (!game.state && !received.state) {
+ console.log(`ping received with no game. Sending update request`);
+ send({
+ type: 'game-update'
+ });
+ }
+ return;
+
default:
console.log(data);
break;
}
}
-const ai = async (ws) => {
+const ai = async () => {
+ send({
+ type: 'get',
+ fields: [ 'dice', 'name', 'color', 'state', 'placements' ]
+ });
}
-connect().then((ws) => {
- ai(ws)
+connect().then(() => {
+ ai()
.catch((error) => {
console.error(error);
ws.close();
diff --git a/server/ai/longest-road.js b/server/ai/longest-road.js
new file mode 100644
index 0000000..d890987
--- /dev/null
+++ b/server/ai/longest-road.js
@@ -0,0 +1,161 @@
+const layout = require('../util/layout.js');
+
+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 0;
+ }
+ /* If this corner is already being walked, skip it */
+ if (placedCorner.walking) {
+ return 0;
+ }
+
+ 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];
+ if (placedRoad.walking) {
+ return;
+ }
+ const tmp = processRoad(game, color, roadIndex, placedRoad);
+ longest = Math.max(tmp, longest);
+ /*if (tmp > longest) {
+ longest = tmp;
+ placedCorner.longestRoad = roadIndex;
+ placedCorner.longest
+ }
+ longest = Math.max(
+ */
+ });
+
+ 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 0;
+ }
+
+ /* If this road is already being walked, skip it */
+ if (placedRoad.walking) {
+ return 0;
+ }
+
+ placedRoad.walking = true;
+ /* Calculate the longest road branching from both corners */
+ let roadLength = 1;
+ layout.roads[roadIndex].corners.forEach(cornerIndex => {
+ const placedCorner = game.placements.corners[cornerIndex];
+ if (placedCorner.walking) {
+ return;
+ }
+ roadLength += processCorner(game, color, cornerIndex, placedCorner);
+ });
+
+ return roadLength;
+};
+
+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 clearRoadWalking = (game) => {
+ /* Clear out walk markers on roads */
+ layout.roads.forEach((item, itemIndex) => {
+ delete game.placements.roads[itemIndex].walking;
+ });
+
+ /* Clear out walk markers on corners */
+ layout.corners.forEach((item, itemIndex) => {
+ delete game.placements.corners[itemIndex].walking;
+ });
+}
+
+const calculateRoadLengths = (game) => {
+ const color = game.color;
+ clearRoadWalking(game);
+
+ /* 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((_, roadIndex) => {
+ const placedRoad = game.placements.roads[roadIndex];
+ if (placedRoad.color === color) {
+ let set = [];
+ buildRoadGraph(game, color, roadIndex, placedRoad, set);
+ if (set.length) {
+ graphs.push({ color, set });
+ }
+ }
+ });
+
+ let final = {
+ segments: 0,
+ index: -1
+ };
+
+ clearRoadWalking(game);
+ graphs.forEach(graph => {
+ graph.longestRoad = 0;
+ graph.set.forEach(roadIndex => {
+ const placedRoad = game.placements.roads[roadIndex];
+ clearRoadWalking(game);
+ const length = processRoad(game, color, roadIndex, placedRoad);
+ if (length >= graph.longestRoad) {
+ graph.longestStartSegment = roadIndex;
+ graph.longestRoad = length;
+ if (length > final.segments) {
+ final.segments = length;
+ final.index = roadIndex;
+ }
+ }
+ });
+ });
+
+ game.placements.roads.forEach(road => delete road.walking);
+
+ return final;
+};
+
+module.exports = calculateRoadLengths;
\ No newline at end of file
diff --git a/server/routes/games.js b/server/routes/games.js
index 883472c..3280a34 100755
--- a/server/routes/games.js
+++ b/server/routes/games.js
@@ -8,12 +8,17 @@ const express = require("express"),
accessSync = fs.accessSync,
randomWords = require("random-words"),
equal = require("fast-deep-equal");
-const layout = require('./layout.js');
+const layout = require('../util/layout.js');
+
+const { getValidRoads, getValidCorners, isRuleEnabled } = require('../util/validLocations.js');
+
const MAX_SETTLEMENTS = 5;
const MAX_CITIES = 4;
const MAX_ROADS = 15;
+const types = [ 'wheat', 'stone', 'sheep', 'wood', 'brick' ];
+
const debug = {
audio: false,
get: true,
@@ -419,10 +424,7 @@ const pickRobber = (game) => {
}
}
-const isRuleEnabled = (game, rule) => {
- return rule in game.rules && game.rules[rule].enabled;
-};
-
+
const processRoll = (game, session, dice) => {
if (!dice[1]) {
console.error(`Invalid roll sequence!`);
@@ -1565,115 +1567,6 @@ const calculateRoadLengths = (game, session) => {
}
};
-const getValidCorners = (game, color, type) => {
- const limits = [];
-
- /* For each corner, if the corner already has a color set, skip it if type
- * isn't set. If type is set, if it is a match, and the color is a match,
- * add it to the list.
- *
- * 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.
- *
- * If still valid, and we are in initial settlement placement, and if
- * Volcano is enabled, verify the tile is not the Volcano.
- */
- layout.corners.forEach((corner, cornerIndex) => {
- const placement = game.placements.corners[cornerIndex];
- if (type) {
- if (placement.color === color && placement.type === type) {
- limits.push(cornerIndex);
- }
- return;
- }
-
- if (placement.color) {
- return;
- }
-
- 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++) {
- /* This side of the road is pointing to the corner being validated.
- * Skip it. */
- if (road.corners[c] === cornerIndex) {
- continue;
- }
- /* There is a settlement within one segment from this
- * corner, so it is invalid for settlement placement */
- if (game.placements.corners[road.corners[c]].color) {
- valid = false;
- }
- }
- }
- if (valid) {
- /* During initial placement, if volcano is enabled, do not allow
- * placement on a corner connected to the volcano (robber starts
- * on the volcano) */
- if (!(game.state === 'initial-placement'
- && isRuleEnabled(game, 'volcano')
- && layout.tiles[game.robber].corners.indexOf(cornerIndex) !== -1
- )) {
- limits.push(cornerIndex);
- }
- }
- });
-
- 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;
-}
-
const isCompatibleOffer = (player, offer) => {
const isBank = offer.name === 'The bank';
let valid = player.gets.length === offer.gives.length &&
@@ -1699,7 +1592,7 @@ const isCompatibleOffer = (player, offer) => {
return;
}
valid = offer.gives.find(item =>
- (item.type === get.type || item.type === '*') &&
+ (item.type === get.type || isBank) &&
item.count === get.count) !== undefined;
});
@@ -1708,7 +1601,7 @@ const isCompatibleOffer = (player, offer) => {
return;
}
valid = offer.gets.find(item =>
- (item.type === give.type || item.type === 'bank') &&
+ (item.type === give.type || isBank) &&
item.count === give.count) !== undefined;
});
return valid;
@@ -1773,6 +1666,11 @@ const checkPlayerOffer = (game, player, offer) => {
return;
}
+ if (give.count <= 0) {
+ error = `${give.count} must be more than 0!`
+ return;
+ }
+
if (player[give.type] < give.count) {
error = `${name} does do not have ${give.count} ${give.type}!`;
return;
@@ -1788,6 +1686,10 @@ const checkPlayerOffer = (game, player, offer) => {
if (error) {
return;
}
+ if (get.count <= 0) {
+ error = `${get.count} must be more than 0!`;
+ return;
+ }
if (offer.gives.find(give => get.type === give.type)) {
error = `${name} can not give and get the same resource type!`;
};
@@ -1800,10 +1702,10 @@ const canMeetOffer = (player, offer) => {
for (let i = 0; i < offer.gets.length; i++) {
const get = offer.gets[i];
if (get.type === 'bank') {
- if (player[player.gives[0].type] < get.count) {
+ if (player[player.gives[0].type] < get.count || get.count <= 0) {
return false;
}
- } else if (player[get.type] < get.count) {
+ } else if (player[get.type] < get.count || get.count <= 0) {
return false;
}
}
@@ -1900,183 +1802,229 @@ router.put("/:id/:action/:value?", async (req, res) => {
return res.status(400).send(error);
});
-const trade = (game, session, action, offer) => {
- const name = session.name, player = session.player;
- let warning;
+const startTrade = (game, session) => {
+ /* Only the active player can begin trading */
+ if (game.turn.name !== session.name) {
+ return `You cannot start trading negotiations when it is not your turn.`
+ }
+ /* Clear any free gives if the player begins trading */
+ if (game.turn.free) {
+ delete game.turn.free;
+ }
+ 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;
+ }
+ addActivity(game, session,
+ `${session.name} requested to begin trading negotiations.`);
+};
+const cancelTrade = (game, session) => {
+ /* TODO: Perhaps 'cancel' is how a player can remove an offer... */
+ if (game.turn.name !== session.name) {
+ return `Only the active player can cancel trading negotiations.`;
+ }
+ game.turn.actions = [];
+ game.turn.limits = {};
+ addActivity(game, session, `${session.name} has cancelled trading negotiations.`);
+};
+
+const processOffer = (game, session, offer) => {
+ let warning = checkPlayerOffer(game, session.player, offer);
+ if (warning) {
+ return warning;
+ }
+
+ if (isSameOffer(session.player, offer)) {
+ console.log(session.player);
+ return `You already have a pending offer submitted for ${offerToString(offer)}.`;
+ }
+
+ session.player.gives = offer.gives;
+ session.player.gets = offer.gets;
+ session.player.offerRejected = {};
+
+ if (game.turn.color === session.color) {
+ game.turn.offer = offer;
+ }
+
+ /* If this offer matches what another player wants, clear rejection
+ * on of that other player's offer */
+ for (let color in game.players) {
+ if (color === session.color) {
+ continue;
+ }
+ const other = game.players[color];
+ if (other.status !== 'Active') {
+ continue;
+ }
+ /* Comparison reverses give/get order */
+ if (isSameOffer(other, { gives: offer.gets, gets: offer.gives })) {
+ if (other.offerRejected) {
+ delete other.offerRejected[session.color];
+ }
+ }
+ }
+
+ addActivity(game, session, `${session.name} submitted an offer to give ${offerToString(offer)}.`);
+};
+
+const rejectOffer = (game, session, offer) => {
+ /* If the active player rejected an offer, they rejected another player */
+ const other = game.players[offer.color];
+ if (!other.offerRejected) {
+ other.offerRejected = {};
+ }
+ other.offerRejected[session.color] = true;
+ if (!session.player.offerRejected) {
+ session.player.offerRejected = {};
+ }
+ session.player.offerRejected[offer.color] = true;
+ addActivity(game, session, `${session.name} rejected ${other.name}'s offer.`);
+};
+
+const acceptOffer = (game, session, offer) => {
+ const name = session.name;
+
+ if (game.turn.name !== name) {
+ return `Only the active player can accept an offer.`;
+ }
+
+ let target;
+
+ console.log({ description: offerToString(offer) });
+
+ let warning = checkPlayerOffer(game, session.player, offer);
+ if (warning) {
+ return warning;
+ }
+
+ if (!isCompatibleOffer(session.player, {
+ name: offer.name,
+ gives: offer.gets,
+ gets: offer.gives
+ })) {
+ return `Unfortunately, trades were re-negotiated in transit and 1 ` +
+ `the deal is invalid!`;
+ }
+
+ /* 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') {
+ target = game.players[offer.color];
+ if (offer.color in target.offerRejected) {
+ return `${target.name} rejected this offer.`;
+ }
+ if (!isCompatibleOffer(target, offer)) {
+ return `Unfortunately, trades were re-negotiated in transit and ` +
+ `the deal is invalid!`;
+ }
+
+ warning = checkPlayerOffer(game, target, {
+ gives: offer.gets,
+ gets: offer.gives
+ });
+ if (warning) {
+ return warning;
+ }
+
+ if (!isSameOffer(target, { gives: offer.gets, gets: offer.gives })) {
+ console.log({ target, offer });
+ return `These terms were not agreed to by ${target.name}!`;
+ }
+
+ if (!canMeetOffer(target, player)) {
+ return `${target.name} cannot meet the terms.`;
+ }
+ } else {
+ target = offer;
+ }
+
+ debugChat(game, 'Before trade');
+
+ /* Transfer goods */
+ const player = session.player;
+ offer.gets.forEach(item => {
+ if (target.name !== 'The bank') {
+ target[item.type] -= item.count;
+ target.resources -= item.count;
+ }
+ player[item.type] += item.count;
+ player.resources += item.count;
+ });
+ offer.gives.forEach(item => {
+ if (target.name !== 'The bank') {
+ target[item.type] += item.count;
+ target.resources += item.count;
+ }
+ player[item.type] -= item.count;
+ player.resources -= item.count;
+ });
+
+ const from = (offer.name === 'The bank') ? 'the bank' : offer.name;
+ addChatMessage(game, session, `${session.name} traded ` +
+ ` ${offerToString(offer)} ` +
+ `from ${from}.`);
+ addActivity(game, session, `${session.name} accepted a trade from ${from}.`)
+ delete game.turn.offer;
+ if (target) {
+ delete target.gives;
+ delete target.gets;
+ }
+ delete session.player.gives;
+ delete session.player.gets;
+ delete game.turn.offer;
+
+ debugChat(game, 'After trade');
+
+ /* Debug!!! */
+ for (let key in game.players) {
+ if (!game.players[key].state === 'Active') {
+ continue;
+ }
+ types.forEach(type => {
+ if (game.players[key][type] < 0) {
+ throw new Error(`Player resources are below zero! BUG BUG BUG!`);
+ }
+ });
+ }
+ game.turn.actions = [];
+};
+
+const trade = (game, session, action, offer) => {
if (game.state !== "normal") {
return `Game not in correct state to begin trading.`;
}
if (!game.turn.actions || game.turn.actions.indexOf('trade') === -1) {
- /* Only the active player can begin trading */
- if (game.turn.name !== name) {
- return `You cannot start trading negotiations when it is not your turn.`
- }
- /* Clear any free gives if the player begins trading */
- if (game.turn.free) {
- delete game.turn.free;
- }
- 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;
- }
- addActivity(game, session, `${name} requested to begin trading negotiations.`);
- return;
+ return startTrade(game, session);
}
/* Only the active player can cancel trading */
if (action === 'cancel') {
- /* TODO: Perhaps 'cancel' is how a player can remove an offer... */
- if (game.turn.name !== name) {
- return `Only the active player can cancel trading negotiations.`;
- }
- game.turn.actions = [];
- game.turn.limits = {};
- addActivity(game, session, `${name} has cancelled trading negotiations.`);
- return;
+ return cancelTrade(game, session);
}
/* Any player can make an offer */
if (action === 'offer') {
- warning = checkPlayerOffer(game, session.player, offer);
- if (warning) {
- return warning;
- }
-
- if (isSameOffer(session.player, offer)) {
- console.log(session.player);
- return `You already have a pending offer submitted for ${offerToString(offer)}.`;
- }
-
- session.player.gives = offer.gives;
- session.player.gets = offer.gets;
- session.player.offerRejected = {};
-
- if (game.turn.color === session.color) {
- game.turn.offer = offer;
- }
-
- /* If this offer matches what another player wants, clear rejection
- * on of that other player's offer */
- for (let color in game.players) {
- if (color === session.color) {
- continue;
- }
- const other = game.players[color];
- if (other.status !== 'Active') {
- continue;
- }
- /* Comparison reverses give/get order */
- if (isSameOffer(other, { gives: offer.gets, gets: offer.gives })) {
- if (other.offerRejected) {
- delete other.offerRejected[session.color];
- }
- }
- }
-
- addActivity(game, session, `${session.name} submitted an offer to give ${offerToString(offer)}.`);
- return;
+ return processOffer(game, session, offer);
}
/* Any player can reject an offer */
if (action === 'reject') {
- /* If the active player rejected an offer, they rejected another player */
- const other = game.players[offer.color];
- if (!other.offerRejected) {
- other.offerRejected = {};
- }
- other.offerRejected[session.color] = true;
- if (!session.player.offerRejected) {
- session.player.offerRejected = {};
- }
- session.player.offerRejected[offer.color] = true;
- addActivity(game, session, `${session.name} rejected ${other.name}'s offer.`);
- return;
+ return rejectOffer(game, session, offer);
}
/* Only the active player can accept an offer */
if (action === 'accept') {
- if (game.turn.name !== name) {
- return `Only the active player can accept an offer.`;
+ if (offer.name === 'The bank') {
+ session.player.gets = offer.gets;
+ session.player.gives = offer.gives;
}
-
- let target;
-
- console.log({ offer, description: offerToString(offer) });
-
- warning = checkPlayerOffer(game, session.player, offer);
- if (warning) {
- return warning;
- }
-
- /* 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') {
- target = game.players[offer.color];
- if (offer.color in target.offerRejected) {
- return `${target.name} rejected this offer.`;
- }
- if (!isCompatibleOffer(target, offer)) {
- return `Unfortunately, trades were re-negotiated in transit and the deal is invalid!`;
- }
-
- warning = checkPlayerOffer(game, target, { gives: offer.gets, gets: offer.gives });
- if (warning) {
- return warning;
- }
-
- if (!isSameOffer(target, { gives: offer.gets, gets: offer.gives })) {
- console.log( { target, offer });
- return `These terms were not agreed to by ${target.name}!`;
- }
-
- if (!canMeetOffer(target, player)) {
- return `${target.name} cannot meet the terms.`;
- }
- } else {
- target = offer;
- }
-
- debugChat(game, 'Before trade');
-
- /* Transfer goods */
- offer.gets.forEach(item => {
- if (target.name !== 'The bank') {
- target[item.type] -= item.count;
- target.resources -= item.count;
- }
- player[item.type] += item.count;
- player.resources += item.count;
- });
- offer.gives.forEach(item => {
- if (target.name !== 'The bank') {
- target[item.type] += item.count;
- target.resources += item.count;
- }
- player[item.type] -= item.count;
- player.resources -= item.count;
- });
-
- const from = (offer.name === 'The bank') ? 'the bank' : offer.name;
- addChatMessage(game, session, `${session.name} traded ` +
- ` ${offerToString(offer)} ` +
- `from ${from}.`);
- addActivity(game, session, `${session.name} accepted a trade from ${from}.`)
- delete game.turn.offer;
- if (target) {
- delete target.gives;
- delete target.gets;
- }
- delete session.player.gives;
- delete session.player.gets;
-
- debugChat(game, 'After trade');
-
- game.turn.actions = [];
+ return acceptOffer(game, session, offer);
}
}
@@ -3044,7 +2992,8 @@ const buyRoad = (game, session) => {
addActivity(game, session, `${game.turn.name} is considering building a road.`);
sendUpdateToPlayers(game, {
turn: game.turn,
- chat: game.chat
+ chat: game.chat,
+ activities: game.activities
});
}
@@ -4316,7 +4265,7 @@ router.ws("/ws/:id", async (ws, req) => {
break;
case 'trade':
console.log(`${short}: <- trade:${getName(session)} - ` +
- (data.action ? data.action : 'start') + ` - `,
+ (data.action ? data.action : 'start') + ` -`,
data.offer ? data.offer : 'no trade yet');
warning = trade(game, session, data.action, data.offer);
if (warning) {
diff --git a/server/routes/layout.js b/server/util/layout.js
similarity index 100%
rename from server/routes/layout.js
rename to server/util/layout.js
diff --git a/server/util/validLocations.js b/server/util/validLocations.js
new file mode 100644
index 0000000..1b65394
--- /dev/null
+++ b/server/util/validLocations.js
@@ -0,0 +1,120 @@
+const layout = require('./layout.js');
+
+const isRuleEnabled = (game, rule) => {
+ return rule in game.rules && game.rules[rule].enabled;
+};
+
+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;
+}
+
+const getValidCorners = (game, color, type) => {
+ const limits = [];
+
+ /* For each corner, if the corner already has a color set, skip it if type
+ * isn't set. If type is set, if it is a match, and the color is a match,
+ * add it to the list.
+ *
+ * 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.
+ *
+ * If still valid, and we are in initial settlement placement, and if
+ * Volcano is enabled, verify the tile is not the Volcano.
+ */
+ layout.corners.forEach((corner, cornerIndex) => {
+ const placement = game.placements.corners[cornerIndex];
+ if (type) {
+ if (placement.color === color && placement.type === type) {
+ limits.push(cornerIndex);
+ }
+ return;
+ }
+
+ if (placement.color) {
+ return;
+ }
+
+ 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++) {
+ /* This side of the road is pointing to the corner being validated.
+ * Skip it. */
+ if (road.corners[c] === cornerIndex) {
+ continue;
+ }
+ /* There is a settlement within one segment from this
+ * corner, so it is invalid for settlement placement */
+ if (game.placements.corners[road.corners[c]].color) {
+ valid = false;
+ }
+ }
+ }
+ if (valid) {
+ /* During initial placement, if volcano is enabled, do not allow
+ * placement on a corner connected to the volcano (robber starts
+ * on the volcano) */
+ if (!(game.state === 'initial-placement'
+ && isRuleEnabled(game, 'volcano')
+ && layout.tiles[game.robber].corners.indexOf(cornerIndex) !== -1
+ )) {
+ limits.push(cornerIndex);
+ }
+ }
+ });
+
+ return limits;
+}
+
+module.exports = {
+ getValidCorners,
+ getValidRoads,
+ isRuleEnabled
+};
\ No newline at end of file