Updated AI
Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
This commit is contained in:
parent
446d4d49e2
commit
1916ad3509
@ -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(<div key={name} className="Requirement">{name} must discard <b>{player.mustDiscard}</b> cards.</div>);
|
||||
}
|
||||
|
||||
const list = activities
|
||||
.filter(activity => timestamp - activity.date < 11000)
|
||||
.map(activity => {
|
||||
return <Activity key={activity.date} activity={activity}/>;
|
||||
let list = activities
|
||||
.filter((activity, index) =>
|
||||
activities.length - 1 === index || timestamp - activity.date < 11000);
|
||||
list = list.map((activity, index) => {
|
||||
return <Activity keep={list.length - 1 === index} key={activity.date} activity={activity}/>;
|
||||
});
|
||||
|
||||
let who;
|
||||
|
686
server/ai/app.js
686
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,26 +110,181 @@ 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 === '') {
|
||||
const types = [ 'wheat', 'brick', 'stone', 'sheep', 'wood' ];
|
||||
|
||||
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 (!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 });
|
||||
return;
|
||||
/* Wait for the game.name to be set to 'name' and for unselected */
|
||||
return { name, players: anyValue, unselected: anyValue };
|
||||
}
|
||||
|
||||
if (game.state !== 'lobby') {
|
||||
return;
|
||||
if (!received.unselected) {
|
||||
return {
|
||||
unselected: anyValue
|
||||
};
|
||||
}
|
||||
|
||||
if (game.unselected.indexOf(name) === -1) {
|
||||
return;
|
||||
/* 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 = [];
|
||||
@ -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,75 +310,13 @@ 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));
|
||||
};
|
||||
let trying = false;
|
||||
if (game.private.settlements
|
||||
&& game.private.wood
|
||||
&& game.private.brick
|
||||
&& game.private.sheep
|
||||
&& game.private.wheat) {
|
||||
send({
|
||||
type: 'buy-settlement'
|
||||
});
|
||||
trying = true;
|
||||
}
|
||||
|
||||
if (game.private.wood && game.private.brick && game.private.roads) {
|
||||
send({
|
||||
type: 'buy-road'
|
||||
});
|
||||
trying = true;
|
||||
}
|
||||
|
||||
return trying;
|
||||
};
|
||||
|
||||
|
||||
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));
|
||||
};
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
console.log(`state - ${game.state}`);
|
||||
|
||||
switch (game.state) {
|
||||
case undefined:
|
||||
case 'lobby':
|
||||
createPlayer(ws);
|
||||
break;
|
||||
|
||||
case 'game-order':
|
||||
const processGameOrder = async () => {
|
||||
if (!game.color) {
|
||||
console.log(`game-order - player not active`);
|
||||
return;
|
||||
return { color };
|
||||
}
|
||||
console.log(`game-order - `, {
|
||||
color: game.color,
|
||||
@ -210,23 +326,43 @@ const message = async (ws, data) => {
|
||||
console.log(`Time to roll as ${game.color}`);
|
||||
send({ type: 'roll' });
|
||||
}
|
||||
break;
|
||||
|
||||
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;
|
||||
return { turn: { color: game.color }};
|
||||
};
|
||||
|
||||
const processInitialPlacement = async (received) => {
|
||||
if (!game.turn || game.turn.color !== game.color) {
|
||||
return {
|
||||
turn: {
|
||||
color: game.color,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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') {
|
||||
console.log({ roads: game.turn.limits.roads });
|
||||
index = game.turn.limits.roads[Math.floor(
|
||||
Math.random() * game.turn.limits.roads.length)];
|
||||
index = bestRoadPlacement(game);
|
||||
} 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)];
|
||||
}
|
||||
@ -234,18 +370,59 @@ const message = async (ws, data) => {
|
||||
send({
|
||||
type, index
|
||||
});
|
||||
} break;
|
||||
/* 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;
|
||||
}
|
||||
|
||||
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);
|
||||
@ -264,26 +441,188 @@ const message = async (ws, data) => {
|
||||
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];
|
||||
}
|
||||
});
|
||||
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 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: 'trade',
|
||||
action: 'cancel'
|
||||
});
|
||||
return {
|
||||
turn: anyValue
|
||||
};
|
||||
}
|
||||
|
||||
if (!shouldTrade) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (game.turn.color !== game.color) {
|
||||
console.log(`not ${name}'s turn.`)
|
||||
return;
|
||||
const give = {
|
||||
type: enough[Math.floor(Math.random() * enough.length)],
|
||||
count: 4
|
||||
}, get = {
|
||||
type: least.type,
|
||||
count: 1
|
||||
};
|
||||
const offer = {
|
||||
gives: [give],
|
||||
gets: [get]
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
await sleep(1000 + Math.random() * 500);
|
||||
if (game.turn.color !== game.color) {
|
||||
return;
|
||||
/* Initiate offer... */
|
||||
|
||||
if (!received.turn.offer) {
|
||||
console.log(`trade - `, offer);
|
||||
send({
|
||||
type: 'trade',
|
||||
action: 'offer',
|
||||
offer
|
||||
});
|
||||
|
||||
return {
|
||||
private: { offerRejected: anyValue }
|
||||
};
|
||||
}
|
||||
|
||||
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)];
|
||||
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;
|
||||
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) {
|
||||
@ -293,15 +632,9 @@ const message = async (ws, data) => {
|
||||
send({
|
||||
type: 'place-settlement', index
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!game.dice) {
|
||||
console.log(`Rolling...`);
|
||||
send({
|
||||
type: 'roll'
|
||||
});
|
||||
return;
|
||||
return {
|
||||
turn: { color: game.color }
|
||||
};
|
||||
}
|
||||
|
||||
if (game.turn.actions
|
||||
@ -313,7 +646,9 @@ const message = async (ws, data) => {
|
||||
type: 'place-robber',
|
||||
index
|
||||
});
|
||||
return;
|
||||
return {
|
||||
turn: { color: game.color }
|
||||
};
|
||||
}
|
||||
|
||||
if (game.turn.actions && game.turn.actions.indexOf('steal-resource') !== -1) {
|
||||
@ -332,36 +667,129 @@ const message = async (ws, data) => {
|
||||
|
||||
if (game.turn.robberInAction) {
|
||||
console.log({ turn: game.turn });
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
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)) {
|
||||
|
||||
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;
|
||||
default:
|
||||
console.log({ state: game.state, turn: game.turn });
|
||||
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':
|
||||
waitingFor = await processLobby(received);
|
||||
if (waitingFor) {
|
||||
processWaitingFor(waitingFor);
|
||||
}
|
||||
return;
|
||||
|
||||
case 'game-order':
|
||||
waitingFor = await processGameOrder(received);
|
||||
if (waitingFor) {
|
||||
processWaitingFor(waitingFor);
|
||||
}
|
||||
return;
|
||||
|
||||
case 'initial-placement':
|
||||
waitingFor = await processInitialPlacement(received);
|
||||
if (waitingFor) {
|
||||
processWaitingFor(waitingFor);
|
||||
}
|
||||
return;
|
||||
|
||||
case 'normal':
|
||||
waitingFor = await processNormal(received);
|
||||
if (waitingFor) {
|
||||
processWaitingFor(waitingFor);
|
||||
}
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ping':
|
||||
if (!game.state) {
|
||||
if (!game.state && !received.state) {
|
||||
console.log(`ping received with no game. Sending update request`);
|
||||
ws.send(JSON.stringify({
|
||||
send({
|
||||
type: 'game-update'
|
||||
}));
|
||||
});
|
||||
}
|
||||
break;
|
||||
return;
|
||||
|
||||
default:
|
||||
console.log(data);
|
||||
@ -369,11 +797,15 @@ const message = async (ws, data) => {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
161
server/ai/longest-road.js
Normal file
161
server/ai/longest-road.js
Normal file
@ -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;
|
@ -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,9 +424,6 @@ const pickRobber = (game) => {
|
||||
}
|
||||
}
|
||||
|
||||
const isRuleEnabled = (game, rule) => {
|
||||
return rule in game.rules && game.rules[rule].enabled;
|
||||
};
|
||||
|
||||
const processRoll = (game, session, dice) => {
|
||||
if (!dice[1]) {
|
||||
@ -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,17 +1802,9 @@ 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;
|
||||
|
||||
if (game.state !== "normal") {
|
||||
return `Game not in correct state to begin trading.`;
|
||||
}
|
||||
|
||||
if (!game.turn.actions || game.turn.actions.indexOf('trade') === -1) {
|
||||
const startTrade = (game, session) => {
|
||||
/* Only the active player can begin trading */
|
||||
if (game.turn.name !== name) {
|
||||
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 */
|
||||
@ -1924,25 +1818,22 @@ const trade = (game, session, action, offer) => {
|
||||
game.players[key].gets = [];
|
||||
delete game.players[key].offerRejected;
|
||||
}
|
||||
addActivity(game, session, `${name} requested to begin trading negotiations.`);
|
||||
return;
|
||||
}
|
||||
addActivity(game, session,
|
||||
`${session.name} requested to begin trading negotiations.`);
|
||||
};
|
||||
|
||||
/* Only the active player can cancel trading */
|
||||
if (action === 'cancel') {
|
||||
const cancelTrade = (game, session) => {
|
||||
/* TODO: Perhaps 'cancel' is how a player can remove an offer... */
|
||||
if (game.turn.name !== name) {
|
||||
if (game.turn.name !== session.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;
|
||||
}
|
||||
addActivity(game, session, `${session.name} has cancelled trading negotiations.`);
|
||||
};
|
||||
|
||||
/* Any player can make an offer */
|
||||
if (action === 'offer') {
|
||||
warning = checkPlayerOffer(game, session.player, offer);
|
||||
const processOffer = (game, session, offer) => {
|
||||
let warning = checkPlayerOffer(game, session.player, offer);
|
||||
if (warning) {
|
||||
return warning;
|
||||
}
|
||||
@ -1979,11 +1870,9 @@ const trade = (game, session, action, offer) => {
|
||||
}
|
||||
|
||||
addActivity(game, session, `${session.name} submitted an offer to give ${offerToString(offer)}.`);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/* Any player can reject an offer */
|
||||
if (action === 'reject') {
|
||||
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) {
|
||||
@ -1995,24 +1884,33 @@ const trade = (game, session, action, offer) => {
|
||||
}
|
||||
session.player.offerRejected[offer.color] = true;
|
||||
addActivity(game, session, `${session.name} rejected ${other.name}'s offer.`);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const acceptOffer = (game, session, offer) => {
|
||||
const name = session.name;
|
||||
|
||||
/* 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.`;
|
||||
}
|
||||
|
||||
let target;
|
||||
|
||||
console.log({ offer, description: offerToString(offer) });
|
||||
console.log({ description: offerToString(offer) });
|
||||
|
||||
warning = checkPlayerOffer(game, session.player, 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') {
|
||||
@ -2021,10 +1919,14 @@ const trade = (game, session, action, offer) => {
|
||||
return `${target.name} rejected this offer.`;
|
||||
}
|
||||
if (!isCompatibleOffer(target, offer)) {
|
||||
return `Unfortunately, trades were re-negotiated in transit and the deal is invalid!`;
|
||||
return `Unfortunately, trades were re-negotiated in transit and ` +
|
||||
`the deal is invalid!`;
|
||||
}
|
||||
|
||||
warning = checkPlayerOffer(game, target, { gives: offer.gets, gets: offer.gives });
|
||||
warning = checkPlayerOffer(game, target, {
|
||||
gives: offer.gets,
|
||||
gets: offer.gives
|
||||
});
|
||||
if (warning) {
|
||||
return warning;
|
||||
}
|
||||
@ -2044,6 +1946,7 @@ const trade = (game, session, action, 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;
|
||||
@ -2073,10 +1976,55 @@ const trade = (game, session, action, offer) => {
|
||||
}
|
||||
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) {
|
||||
return startTrade(game, session);
|
||||
}
|
||||
|
||||
/* Only the active player can cancel trading */
|
||||
if (action === 'cancel') {
|
||||
return cancelTrade(game, session);
|
||||
}
|
||||
|
||||
/* Any player can make an offer */
|
||||
if (action === 'offer') {
|
||||
return processOffer(game, session, offer);
|
||||
}
|
||||
|
||||
/* Any player can reject an offer */
|
||||
if (action === 'reject') {
|
||||
return rejectOffer(game, session, offer);
|
||||
}
|
||||
|
||||
/* Only the active player can accept an offer */
|
||||
if (action === 'accept') {
|
||||
if (offer.name === 'The bank') {
|
||||
session.player.gets = offer.gets;
|
||||
session.player.gives = offer.gives;
|
||||
}
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
|
120
server/util/validLocations.js
Normal file
120
server/util/validLocations.js
Normal file
@ -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
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user