@screeps/engine
Version:
This is a module for Screeps standalone server. See [main repository](https://github.com/screeps/screeps) for more info.
712 lines (620 loc) • 21 kB
JavaScript
var _ = require('lodash');
var driver, C, offsetsByDirection = [, [0,-1], [1,-1], [1,0], [1,1], [0,1], [-1,1], [-1,0], [-1,-1]];
function loadDriver() {
C = driver.constants;
}
try {
driver = require('~runtime-driver');
loadDriver();
}
catch(e) {}
exports.getDriver = function getDriver() {
driver = typeof process != 'undefined' && process.env.DRIVER_MODULE ?
require(process.env.DRIVER_MODULE) :
require('@screeps/core');
loadDriver();
return driver;
};
exports.getRuntimeDriver = function getRuntimeDriver() {
try {
driver = require('~runtime-driver');
loadDriver();
return driver;
}
catch (e) {
return exports.getDriver();
}
};
exports.fetchXYArguments = function(firstArg, secondArg, globals) {
var x,y, roomName;
if(_.isUndefined(secondArg) || !_.isNumber(secondArg)) {
if(!_.isObject(firstArg)) {
return [undefined,undefined,undefined];
}
if(firstArg instanceof globals.RoomPosition) {
x = firstArg.x;
y = firstArg.y;
roomName = firstArg.roomName;
}
if(firstArg.pos && (firstArg.pos instanceof globals.RoomPosition)) {
x = firstArg.pos.x;
y = firstArg.pos.y;
roomName = firstArg.pos.roomName;
}
}
else {
x = firstArg;
y = secondArg;
}
if(_.isNaN(x)) {
x = undefined;
}
if(_.isNaN(y)) {
y = undefined;
}
return [x,y,roomName];
};
exports.getDirection = function(dx, dy) {
var adx = Math.abs(dx), ady = Math.abs(dy);
if(adx > ady*2) {
if(dx > 0) {
return C.RIGHT;
}
else {
return C.LEFT;
}
}
else if(ady > adx*2) {
if(dy > 0) {
return C.BOTTOM;
}
else {
return C.TOP;
}
}
else {
if(dx > 0 && dy > 0) {
return C.BOTTOM_RIGHT;
}
if(dx > 0 && dy < 0) {
return C.TOP_RIGHT;
}
if(dx < 0 && dy > 0) {
return C.BOTTOM_LEFT;
}
if(dx < 0 && dy < 0) {
return C.TOP_LEFT;
}
}
};
exports.getOffsetsByDirection = function(direction) {
if(!offsetsByDirection[direction]) {
try {
throw new Error();
}
catch(e) {
console.error('Wrong move direction',JSON.stringify(direction), JSON.stringify(offsetsByDirection), e.stack);
}
}
return offsetsByDirection[direction];
};
exports.calcCreepCost = function(body) {
let result = 0;
body.forEach((i) => {
const partType = _.isObject(i) ? i.type : i;
if(!C.BODYPART_COST[partType]) {
throw new Error(`Invalid body part ${partType}`);
}
result += C.BODYPART_COST[partType];
});
return result;
};
exports.checkConstructionSite = function(objects, structureType, x, y) {
var borderTiles;
if(structureType != 'road' && structureType != 'container' && (x == 1 || x == 48 || y == 1 || y == 48)) {
if(x == 1) borderTiles = [[0,y-1],[0,y],[0,y+1]];
if(x == 48) borderTiles = [[49,y-1],[49,y],[49,y+1]];
if(y == 1) borderTiles = [[x-1,0],[x,0],[x+1,0]];
if(y == 48) borderTiles = [[x-1,49],[x,49],[x+1,49]];
}
if(_.isString(objects) || objects instanceof Uint8Array) {
if(borderTiles) {
for(var i in borderTiles) {
if(!exports.checkTerrain(objects, borderTiles[i][0], borderTiles[i][1], C.TERRAIN_MASK_WALL)) {
return false;
}
}
}
if(structureType == 'extractor') {
return true;
}
if(structureType != 'road' && exports.checkTerrain(objects, x, y, C.TERRAIN_MASK_WALL)) {
return false;
}
return true;
}
if(objects && _.isArray(objects[0]) && _.isString(objects[0][0])) {
if(borderTiles) {
for(var i in borderTiles) {
if(!(objects[borderTiles[i][1]][borderTiles[i][0]] & C.TERRAIN_MASK_WALL)) {
return false;
}
}
}
if(structureType == 'extractor') {
return true;
}
if(structureType != 'road' && objects[y][x] & C.TERRAIN_MASK_WALL) {
return false;
}
return true;
}
if(_.any(objects, {x, y, type: structureType})) {
return false;
}
if(_.any(objects, {x, y, type: 'constructionSite'})) {
return false;
}
if(structureType == 'extractor') {
return _.any(objects, {x, y, type: 'mineral'}) && !_.any(objects, {x, y, type: 'extractor'});
}
if(structureType != 'rampart' && structureType != 'road' &&
_.any(objects, (i) => i.x == x && i.y == y && i.type != 'rampart' && i.type != 'road' && C.CONSTRUCTION_COST[i.type])) {
return false;
}
if(x <= 0 || y <= 0 || x >= 49 || y >= 49) {
return false;
}
return true;
};
exports.getDiff = function(oldData, newData) {
function getIndex(data) {
var index = {};
_.forEach(data, (obj) => index[obj._id] = obj);
return index;
}
var result = {},
oldIndex = getIndex(oldData),
newIndex = getIndex(newData);
_.forEach(oldData, (obj) => {
if(newIndex[obj._id]) {
var newObj = newIndex[obj._id];
var objDiff = result[obj._id] = {};
for(var key in obj) {
if(key == '_id') {
continue;
}
if(_.isUndefined(newObj[key])) {
objDiff[key] = null;
}
else if((typeof obj[key]) != (typeof newObj[key]) || obj[key] && !newObj[key]) {
objDiff[key] = newObj[key];
}
else if(_.isObject(obj[key])) {
objDiff[key] = {};
for (var subkey in obj[key]) {
if (!_.isEqual(obj[key][subkey], newObj[key][subkey])) {
objDiff[key][subkey] = newObj[key][subkey];
}
}
for (var subkey in newObj[key]) {
if (_.isUndefined(obj[key][subkey])) {
objDiff[key][subkey] = newObj[key][subkey];
}
}
if (!_.size(objDiff[key])) {
delete result[obj._id][key];
}
}
else if(!_.isEqual(obj[key], newObj[key])) {
objDiff[key] = newObj[key];
}
}
for(var key in newObj) {
if(_.isUndefined(obj[key])) {
objDiff[key] = newObj[key];
}
}
if(!_.size(objDiff)) {
delete result[obj._id];
}
}
else {
result[obj._id] = null;
}
});
_.forEach(newData, (obj) => {
if(!oldIndex[obj._id]) {
result[obj._id] = obj;
}
});
return result;
};
exports.encodeTerrain = function(terrain) {
var result = '';
for(var y=0; y<50; y++) {
for(var x=0; x<50; x++) {
var objects = _.filter(terrain, {x,y}),
code = 0;
if(_.any(objects, {type: 'wall'})) {
code = code | C.TERRAIN_MASK_WALL;
}
if(_.any(objects, {type: 'swamp'})) {
code = code | C.TERRAIN_MASK_SWAMP;
}
result = result + code;
}
}
return result;
};
exports.decodeTerrain = function(items) {
var result = [];
for(var i in items) {
if(items[i].type != 'terrain') {
continue;
}
for (var y = 0; y < 50; y++) {
for (var x = 0; x < 50; x++) {
var code = items[i].terrain.charAt(y * 50 + x);
if (code & C.TERRAIN_MASK_WALL) {
result.push({room: items[i].room, x, y, type: 'wall'});
}
if (code & C.TERRAIN_MASK_SWAMP) {
result.push({room: items[i].room, x, y, type: 'swamp'});
}
}
}
}
return result;
};
exports.decodeTerrainByRoom = function(items) {
var result = {
spatial: {}
};
for(var i in items) {
if(items[i].type != 'terrain') {
continue;
}
result[items[i].room] = result[items[i].room] || [];
result.spatial[items[i].room] = new Array(50);
for (var y = 0; y < 50; y++) {
result.spatial[items[i].room][y] = new Array(50);
for (var x = 0; x < 50; x++) {
var code = items[i].terrain.charAt(y * 50 + x);
/*if (code & C.TERRAIN_MASK_WALL) {
result[items[i].room].push({x, y, type: 'wall'});
}
if (code & C.TERRAIN_MASK_SWAMP) {
result[items[i].room].push({x, y, type: 'swamp'});
}*/
result.spatial[items[i].room][y][x] = code;
}
}
}
return result;
};
exports.checkTerrain = function(terrain, x, y, mask) {
var code = terrain instanceof Uint8Array ? terrain[y*50+x] : Number(terrain.charAt(y*50 + x));
return (code & mask) > 0;
};
exports.checkControllerAvailability = function(type, roomObjects, roomController, offset) {
var rcl = 0;
if(_.isObject(roomController) && roomController.level && (roomController.user || roomController.owner)) {
rcl = roomController.level;
}
if(_.isNumber(roomController)) {
rcl = roomController;
}
offset = offset || 0;
var structuresCnt = _(roomObjects).filter((i) => i.type == type || i.type == 'constructionSite' && i.structureType == type).size();
var availableCnt = C.CONTROLLER_STRUCTURES[type][rcl] + offset;
return structuresCnt < availableCnt;
};
// Note that game/rooms.js will swap this function out for a faster version, but may call back to
// this function.
exports.getRoomNameFromXY = function(x,y) {
if(x < 0) {
x = 'W'+(-x-1);
}
else {
x = 'E'+(x);
}
if(y < 0) {
y = 'N'+(-y-1);
}
else {
y = 'S'+(y);
}
return ""+x+y;
};
exports.roomNameToXY = function(name) {
let xx = parseInt(name.substr(1), 10);
let verticalPos = 2;
if (xx >= 100) {
verticalPos = 4;
} else if (xx >= 10) {
verticalPos = 3;
}
let yy = parseInt(name.substr(verticalPos + 1), 10);
let horizontalDir = name.charAt(0);
let verticalDir = name.charAt(verticalPos);
if (horizontalDir === 'W' || horizontalDir === 'w') {
xx = -xx - 1;
}
if (verticalDir === 'N' || verticalDir === 'n') {
yy = -yy - 1;
}
return [xx, yy];
};
exports.comparatorDistance = function(target) {
if(target.pos) target = target.pos;
return function(a,b) {
if(a.pos) a = a.pos;
if(b.pos) b = b.pos;
var da = Math.max(Math.abs(a.x - target.x), Math.abs(a.y - target.y));
var db = Math.max(Math.abs(b.x - target.x), Math.abs(b.y - target.y));
return da - db;
}
};
exports.storeIntents = function(userId, userIntents, userRuntimeData, customIntentTypes) {
const system = driver ? driver.system : exports.getDriver().system;
var intents = {};
for(var i in userIntents) {
if(i == 'notify') {
intents.notify = system.sanitizeUserIntents({notify: userIntents.notify}).notify;
continue;
}
if(i == 'room') {
system.sanitizeUserRoomIntents(userIntents.room, intents, customIntentTypes);
continue;
}
if(i == 'global') {
intents.global = system.sanitizeUserIntents(userIntents.global, customIntentTypes);
continue;
}
const object = userRuntimeData.userObjects[i] || userRuntimeData.roomObjects[i];
if(!object) {
continue;
}
intents[object.room] = intents[object.room] || {};
intents[object.room][i] = system.sanitizeUserIntents(userIntents[i], customIntentTypes);
}
return intents;
}
exports.sendAttackingNotification = function(target, roomController) {
var driver = exports.getDriver();
var labelText;
if(target.type == 'creep') {
labelText = 'creep '+target.name;
}
else if(target.type == 'spawn') {
labelText = 'spawn '+target.name;
}
else {
labelText = `${target.type} #${target._id}`;
}
var user = target.user ? target.user : roomController ? roomController.user : null;
if(user) {
driver.sendNotification(user, `Your ${labelText} in room ${target.room} is under attack!`);
}
};
exports.checkStructureAgainstController = function(object, roomObjects, roomController) {
// owner-less objects are always active
if(!object.user) {
return true;
}
// eliminate some other easy cases
if(!roomController || roomController.level < 1 || roomController.user !== object.user) {
return false;
}
let allowedRemaining = C.CONTROLLER_STRUCTURES[object.type][roomController.level];
if(allowedRemaining === 0) {
return false;
}
// if only one object ever allowed, this is it
if(C.CONTROLLER_STRUCTURES[object.type][8] === 1) {
return allowedRemaining !== 0;
}
// Scan through the room objects of the same type and count how many are closer.
let foundSelf = false;
let objectDist = Math.max(Math.abs(object.x - roomController.x), Math.abs(object.y - roomController.y));
let objectIds = _.keys(roomObjects);
for (let i = 0; i < objectIds.length; i++) {
let compareObj = roomObjects[objectIds[i]];
if(compareObj.type === object.type && compareObj.user === object.user) {
let compareDist = Math.max(Math.abs(compareObj.x - roomController.x), Math.abs(compareObj.y - roomController.y));
if(compareDist < objectDist) {
allowedRemaining--;
if (allowedRemaining === 0) {
return false;
}
} else if(!foundSelf && compareDist === objectDist) {
// Objects of equal distance that are discovered before we scan over the selected object are considered closer
if(object === compareObj) {
foundSelf = true;
} else {
allowedRemaining--;
if (allowedRemaining === 0) {
return false;
}
}
}
}
}
return true;
};
exports.defineGameObjectProperties = function(obj, dataFn, properties, opts) {
var propertiesInfo = {};
opts = opts || {};
if(opts.enumerable === undefined) {
opts.enumerable = true;
}
for(var name in properties) {
eval(`
propertiesInfo['${name}'] = {
configurable: !!opts.configurable,
enumerable: !!opts.enumerable,
get() {
if(!this['_${name}']) {
this['_${name}'] = properties['${name}'](dataFn(this.id), this.id);
}
return this['_${name}'];
},
set: opts.canSet ? function(value) {
this['_${name}'] = value;
} : undefined
}`);
}
Object.defineProperties(obj, propertiesInfo);
obj.toJSON = function() {
var result = {};
for(var i in this) {
if(i[0] == '_' || _.contains(['toJSON','toString'],i)) {
continue;
}
result[i] = this[i];
}
return result;
}
};
exports.isAtEdge = function(object) {
if(object.pos) {
object = object.pos;
}
return object.x == 0 || object.x == 49 || object.y == 0 || object.y == 49
}
exports.serializePath = function(path) {
if(!_.isArray(path)) {
throw new Error('path is not an array');
}
var result = '';
if(!path.length) {
return result;
}
if(path[0].x < 0 || path[0].y < 0) {
throw new Error('path coordinates cannot be negative');
}
result += path[0].x > 9 ? path[0].x : '0'+path[0].x;
result += path[0].y > 9 ? path[0].y : '0'+path[0].y;
for(var i=0; i<path.length; i++) {
result += path[i].direction;
}
return result;
};
exports.deserializePath = function(path) {
if(!_.isString(path)) {
throw new Error('`path` is not a string');
}
var result = [];
if(!path.length) {
return result;
}
var x,y, direction, dx, dy;
x = parseInt(path.substring(0, 2));
y = parseInt(path.substring(2, 4));
if(_.isNaN(x) || _.isNaN(y)) {
throw new Error('`path` is not a valid serialized path string');
}
for (var i = 4; i < path.length; i++) {
direction = parseInt(path.charAt(i));
if(!offsetsByDirection[direction]) {
throw new Error('`path` is not a valid serialized path string');
}
dx = offsetsByDirection[direction][0];
dy = offsetsByDirection[direction][1];
if (i > 4) {
x += dx;
y += dy;
}
result.push({
x, y,
dx, dy,
direction
});
}
return result;
};
exports.calcResources = function(object) {
if(object.store) {
return _.sum(object.store);
}
return _.sum(C.RESOURCES_ALL, i => typeof object[i] == 'object' ? object[i].amount : (object[i] || 0));
};
exports.calcBodyEffectiveness = function(body, bodyPartType, methodName, basePower, withoutOldHits) {
var power = 0;
body.forEach(i => {
if(!(i.hits || !withoutOldHits && i._oldHits) || i.type != bodyPartType) {
return;
}
var iPower = basePower;
if(i.boost && C.BOOSTS[bodyPartType][i.boost] && C.BOOSTS[bodyPartType][i.boost][methodName]) {
iPower *= C.BOOSTS[bodyPartType][i.boost][methodName];
}
power += iPower;
});
return power;
};
exports.dist = function(a, b) {
if(a.pos) a = a.pos;
if(b.pos) b = b.pos;
return Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y));
};
exports.calcRoomsDistance = function(room1, room2, continuous) {
var [x1,y1] = exports.roomNameToXY(room1);
var [x2,y2] = exports.roomNameToXY(room2);
var dx = Math.abs(x2-x1);
var dy = Math.abs(y2-y1);
if(continuous) {
var worldSize = driver.getWorldSize();
dx = Math.min(worldSize - dx, dx);
dy = Math.min(worldSize - dy, dy);
}
return Math.max(dx, dy);
};
exports.calcTerminalEnergyCost = function(amount, range) {
return Math.ceil(amount * (1 - Math.exp(-range / 30)))
};
exports.calcNeededGcl = function(gclLevel) {
return C.GCL_MULTIPLY * Math.pow(gclLevel-1, C.GCL_POW);
};
exports.calcTotalReactionsTime = function(mineral) {
const reagents = _.reduce(C.REACTIONS, (a,n,j) => { _.forEach(n, (k,v) => a[k] = [v,j]); return a; }, {});
const calcStep = m => !!C.REACTION_TIME[m] ? C.REACTION_TIME[m] + calcStep(reagents[m][0]) + calcStep(reagents[m][1]) : 0;
return calcStep(mineral);
};
exports.capacityForResource = function(object, resourceType) {
return object.storeCapacityResource &&
object.storeCapacityResource[resourceType] ||
Math.max(0, (object.storeCapacity||0) - _.sum(object.storeCapacityResource));
};
exports.calcReward = function(resourceDensities, targetDensity, itemsLimit) {
let resources = [];
let densities = [];
_.forEach(resourceDensities, (density, resource) => {
resources.push(resource);
densities.push(density);
});
let order = _.shuffle(_.range(resources.length));
if(itemsLimit) {
order = order.slice(0, itemsLimit);
}
let result = _.times(order.length, 0);
let currentDensity = 0;
for (let i = 0; i < order.length - 1; i++) {
result[i] = Math.max(0, Math.round(Math.random() * (targetDensity - currentDensity) / densities[order[i]]));
currentDensity += result[i] * densities[order[i]];
}
result[order.length - 1] = Math.max(0, Math.round((targetDensity - currentDensity) / densities[order.length - 1]));
return _.object(order.map(i => resources[i]), result);
};
exports.getReactionVariants = function getReactionVarients(compound) {
const result = [];
for(let r1 in C.REACTIONS) {
for(let r2 in C.REACTIONS[r1]) {
if(C.REACTIONS[r1][r2] == compound) {
result.push([r1, r2]);
}
}
}
return result;
};