1
0

Updated AI

Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
This commit is contained in:
James Ketrenos 2022-06-25 15:27:12 -07:00
parent 446d4d49e2
commit 1916ad3509
6 changed files with 1165 additions and 502 deletions

View File

@ -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;

View File

@ -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
View 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;

View File

@ -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
});
}

View 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
};