screeps/Traveler.js
2020-11-09 19:31:39 -06:00

584 lines
22 KiB
JavaScript

/**
* To start using Traveler, require it in main.js:
* Example: var Traveler = require('Traveler.js');
*/
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class Traveler {
/**
* move creep to destination
* @param creep
* @param destination
* @param options
* @returns {number}
*/
static travelTo(creep, destination, options = {}) {
// uncomment if you would like to register hostile rooms entered
// this.updateRoomStatus(creep.room);
if (!destination) {
return ERR_INVALID_ARGS;
}
if (creep.fatigue > 0) {
Traveler.circle(creep.pos, "aqua", .3);
return ERR_BUSY;
}
destination = this.normalizePos(destination);
// manage case where creep is nearby destination
let rangeToDestination = creep.pos.getRangeTo(destination);
if (options.range && rangeToDestination <= options.range) {
return OK;
}
else if (rangeToDestination <= 1) {
if (rangeToDestination === 1 && !options.range) {
let direction = creep.pos.getDirectionTo(destination);
if (options.returnData) {
options.returnData.nextPos = destination;
options.returnData.path = direction.toString();
}
return creep.move(direction);
}
return OK;
}
// initialize data object
if (!creep.memory._trav) {
delete creep.memory._travel;
creep.memory._trav = {};
}
let travelData = creep.memory._trav;
let state = this.deserializeState(travelData, destination);
// uncomment to visualize destination
// this.circle(destination.pos, "orange");
// check if creep is stuck
if (this.isStuck(creep, state)) {
state.stuckCount++;
Traveler.circle(creep.pos, "magenta", state.stuckCount * .2);
}
else {
state.stuckCount = 0;
}
// handle case where creep is stuck
if (!options.stuckValue) {
options.stuckValue = DEFAULT_STUCK_VALUE;
}
if (state.stuckCount >= options.stuckValue && Math.random() > .5) {
options.ignoreCreeps = false;
options.freshMatrix = true;
delete travelData.path;
}
// TODO:handle case where creep moved by some other function, but destination is still the same
// delete path cache if destination is different
if (!this.samePos(state.destination, destination)) {
if (options.movingTarget && state.destination.isNearTo(destination)) {
travelData.path += state.destination.getDirectionTo(destination);
state.destination = destination;
}
else {
delete travelData.path;
}
}
if (options.repath && Math.random() < options.repath) {
// add some chance that you will find a new path randomly
delete travelData.path;
}
// pathfinding
let newPath = false;
if (!travelData.path) {
newPath = true;
if (creep.spawning) {
return ERR_BUSY;
}
state.destination = destination;
let cpu = Game.cpu.getUsed();
let ret = this.findTravelPath(creep.pos, destination, options);
let cpuUsed = Game.cpu.getUsed() - cpu;
state.cpu = _.round(cpuUsed + state.cpu);
if (state.cpu > REPORT_CPU_THRESHOLD) {
// see note at end of file for more info on this
console.log(`TRAVELER: heavy cpu use: ${creep.name}, cpu: ${state.cpu} origin: ${creep.pos}, dest: ${destination}`);
}
let color = "orange";
if (ret.incomplete) {
// uncommenting this is a great way to diagnose creep behavior issues
// console.log(`TRAVELER: incomplete path for ${creep.name}`);
color = "red";
}
if (options.returnData) {
options.returnData.pathfinderReturn = ret;
}
travelData.path = Traveler.serializePath(creep.pos, ret.path, color);
state.stuckCount = 0;
}
this.serializeState(creep, destination, state, travelData);
if (!travelData.path || travelData.path.length === 0) {
return ERR_NO_PATH;
}
// consume path
if (state.stuckCount === 0 && !newPath) {
travelData.path = travelData.path.substr(1);
}
let nextDirection = parseInt(travelData.path[0], 10);
if (options.returnData) {
if (nextDirection) {
let nextPos = Traveler.positionAtDirection(creep.pos, nextDirection);
if (nextPos) {
options.returnData.nextPos = nextPos;
}
}
options.returnData.state = state;
options.returnData.path = travelData.path;
}
return creep.move(nextDirection);
}
/**
* make position objects consistent so that either can be used as an argument
* @param destination
* @returns {any}
*/
static normalizePos(destination) {
if (!(destination instanceof RoomPosition)) {
return destination.pos;
}
return destination;
}
/**
* check if room should be avoided by findRoute algorithm
* @param roomName
* @returns {RoomMemory|number}
*/
static checkAvoid(roomName) {
return Memory.rooms && Memory.rooms[roomName] && Memory.rooms[roomName].avoid;
}
/**
* check if a position is an exit
* @param pos
* @returns {boolean}
*/
static isExit(pos) {
return pos.x === 0 || pos.y === 0 || pos.x === 49 || pos.y === 49;
}
/**
* check two coordinates match
* @param pos1
* @param pos2
* @returns {boolean}
*/
static sameCoord(pos1, pos2) {
return pos1.x === pos2.x && pos1.y === pos2.y;
}
/**
* check if two positions match
* @param pos1
* @param pos2
* @returns {boolean}
*/
static samePos(pos1, pos2) {
return this.sameCoord(pos1, pos2) && pos1.roomName === pos2.roomName;
}
/**
* draw a circle at position
* @param pos
* @param color
* @param opacity
*/
static circle(pos, color, opacity) {
new RoomVisual(pos.roomName).circle(pos, {
radius: .45, fill: "transparent", stroke: color, strokeWidth: .15, opacity: opacity
});
}
/**
* update memory on whether a room should be avoided based on controller owner
* @param room
*/
static updateRoomStatus(room) {
if (!room) {
return;
}
if (room.controller) {
if (room.controller.owner && !room.controller.my) {
room.memory.avoid = 1;
}
else {
delete room.memory.avoid;
}
}
}
/**
* find a path from origin to destination
* @param origin
* @param destination
* @param options
* @returns {PathfinderReturn}
*/
static findTravelPath(origin, destination, options = {}) {
_.defaults(options, {
ignoreCreeps: true,
maxOps: DEFAULT_MAXOPS,
range: 1,
});
if (options.movingTarget) {
options.range = 0;
}
origin = this.normalizePos(origin);
destination = this.normalizePos(destination);
let originRoomName = origin.roomName;
let destRoomName = destination.roomName;
// check to see whether findRoute should be used
let roomDistance = Game.map.getRoomLinearDistance(origin.roomName, destination.roomName);
let allowedRooms = options.route;
if (!allowedRooms && (options.useFindRoute || (options.useFindRoute === undefined && roomDistance > 2))) {
let route = this.findRoute(origin.roomName, destination.roomName, options);
if (route) {
allowedRooms = route;
}
}
let roomsSearched = 0;
let callback = (roomName) => {
if (allowedRooms) {
if (!allowedRooms[roomName]) {
return false;
}
}
else if (!options.allowHostile && Traveler.checkAvoid(roomName)
&& roomName !== destRoomName && roomName !== originRoomName) {
return false;
}
roomsSearched++;
let matrix;
let room = Game.rooms[roomName];
if (room) {
if (options.ignoreStructures) {
matrix = new PathFinder.CostMatrix();
if (!options.ignoreCreeps) {
Traveler.addCreepsToMatrix(room, matrix);
}
}
else if (options.ignoreCreeps || roomName !== originRoomName) {
matrix = this.getStructureMatrix(room, options.freshMatrix);
}
else {
matrix = this.getCreepMatrix(room);
}
if (options.obstacles) {
matrix = matrix.clone();
for (let obstacle of options.obstacles) {
if (obstacle.pos.roomName !== roomName) {
continue;
}
matrix.set(obstacle.pos.x, obstacle.pos.y, 0xff);
}
}
}
if (options.roomCallback) {
if (!matrix) {
matrix = new PathFinder.CostMatrix();
}
let outcome = options.roomCallback(roomName, matrix.clone());
if (outcome !== undefined) {
return outcome;
}
}
return matrix;
};
let ret = PathFinder.search(origin, { pos: destination, range: options.range }, {
maxOps: options.maxOps,
maxRooms: options.maxRooms,
plainCost: options.offRoad ? 1 : options.ignoreRoads ? 1 : 2,
swampCost: options.offRoad ? 1 : options.ignoreRoads ? 5 : 10,
roomCallback: callback,
});
if (ret.incomplete && options.ensurePath) {
if (options.useFindRoute === undefined) {
// handle case where pathfinder failed at a short distance due to not using findRoute
// can happen for situations where the creep would have to take an uncommonly indirect path
// options.allowedRooms and options.routeCallback can also be used to handle this situation
if (roomDistance <= 2) {
console.log(`TRAVELER: path failed without findroute, trying with options.useFindRoute = true`);
console.log(`from: ${origin}, destination: ${destination}`);
options.useFindRoute = true;
ret = this.findTravelPath(origin, destination, options);
console.log(`TRAVELER: second attempt was ${ret.incomplete ? "not " : ""}successful`);
return ret;
}
// TODO: handle case where a wall or some other obstacle is blocking the exit assumed by findRoute
}
else {
}
}
return ret;
}
/**
* find a viable sequence of rooms that can be used to narrow down pathfinder's search algorithm
* @param origin
* @param destination
* @param options
* @returns {{}}
*/
static findRoute(origin, destination, options = {}) {
let restrictDistance = options.restrictDistance || Game.map.getRoomLinearDistance(origin, destination) + 10;
let allowedRooms = { [origin]: true, [destination]: true };
let highwayBias = 1;
if (options.preferHighway) {
highwayBias = 2.5;
if (options.highwayBias) {
highwayBias = options.highwayBias;
}
}
let ret = Game.map.findRoute(origin, destination, {
routeCallback: (roomName) => {
if (options.routeCallback) {
let outcome = options.routeCallback(roomName);
if (outcome !== undefined) {
return outcome;
}
}
let rangeToRoom = Game.map.getRoomLinearDistance(origin, roomName);
if (rangeToRoom > restrictDistance) {
// room is too far out of the way
return Number.POSITIVE_INFINITY;
}
if (!options.allowHostile && Traveler.checkAvoid(roomName) &&
roomName !== destination && roomName !== origin) {
// room is marked as "avoid" in room memory
return Number.POSITIVE_INFINITY;
}
let parsed;
if (options.preferHighway) {
parsed = /^[WE]([0-9]+)[NS]([0-9]+)$/.exec(roomName);
let isHighway = (parsed[1] % 10 === 0) || (parsed[2] % 10 === 0);
if (isHighway) {
return 1;
}
}
// SK rooms are avoided when there is no vision in the room, harvested-from SK rooms are allowed
if (!options.allowSK && !Game.rooms[roomName]) {
if (!parsed) {
parsed = /^[WE]([0-9]+)[NS]([0-9]+)$/.exec(roomName);
}
let fMod = parsed[1] % 10;
let sMod = parsed[2] % 10;
let isSK = !(fMod === 5 && sMod === 5) &&
((fMod >= 4) && (fMod <= 6)) &&
((sMod >= 4) && (sMod <= 6));
if (isSK) {
return 10 * highwayBias;
}
}
return highwayBias;
},
});
if (!_.isArray(ret)) {
console.log(`couldn't findRoute to ${destination}`);
return;
}
for (let value of ret) {
allowedRooms[value.room] = true;
}
return allowedRooms;
}
/**
* check how many rooms were included in a route returned by findRoute
* @param origin
* @param destination
* @returns {number}
*/
static routeDistance(origin, destination) {
let linearDistance = Game.map.getRoomLinearDistance(origin, destination);
if (linearDistance >= 32) {
return linearDistance;
}
let allowedRooms = this.findRoute(origin, destination);
if (allowedRooms) {
return Object.keys(allowedRooms).length;
}
}
/**
* build a cost matrix based on structures in the room. Will be cached for more than one tick. Requires vision.
* @param room
* @param freshMatrix
* @returns {any}
*/
static getStructureMatrix(room, freshMatrix) {
if (!this.structureMatrixCache[room.name] || (freshMatrix && Game.time !== this.structureMatrixTick)) {
this.structureMatrixTick = Game.time;
let matrix = new PathFinder.CostMatrix();
this.structureMatrixCache[room.name] = Traveler.addStructuresToMatrix(room, matrix, 1);
}
return this.structureMatrixCache[room.name];
}
/**
* build a cost matrix based on creeps and structures in the room. Will be cached for one tick. Requires vision.
* @param room
* @returns {any}
*/
static getCreepMatrix(room) {
if (!this.creepMatrixCache[room.name] || Game.time !== this.creepMatrixTick) {
this.creepMatrixTick = Game.time;
this.creepMatrixCache[room.name] = Traveler.addCreepsToMatrix(room, this.getStructureMatrix(room, true).clone());
}
return this.creepMatrixCache[room.name];
}
/**
* add structures to matrix so that impassible structures can be avoided and roads given a lower cost
* @param room
* @param matrix
* @param roadCost
* @returns {CostMatrix}
*/
static addStructuresToMatrix(room, matrix, roadCost) {
let impassibleStructures = [];
for (let structure of room.find(FIND_STRUCTURES)) {
if (structure instanceof StructureRampart) {
if (!structure.my && !structure.isPublic) {
impassibleStructures.push(structure);
}
}
else if (structure instanceof StructureRoad) {
matrix.set(structure.pos.x, structure.pos.y, roadCost);
}
else if (structure instanceof StructureContainer) {
matrix.set(structure.pos.x, structure.pos.y, 5);
}
else {
impassibleStructures.push(structure);
}
}
for (let site of room.find(FIND_MY_CONSTRUCTION_SITES)) {
if (site.structureType === STRUCTURE_CONTAINER || site.structureType === STRUCTURE_ROAD
|| site.structureType === STRUCTURE_RAMPART) {
continue;
}
matrix.set(site.pos.x, site.pos.y, 0xff);
}
for (let structure of impassibleStructures) {
matrix.set(structure.pos.x, structure.pos.y, 0xff);
}
return matrix;
}
/**
* add creeps to matrix so that they will be avoided by other creeps
* @param room
* @param matrix
* @returns {CostMatrix}
*/
static addCreepsToMatrix(room, matrix) {
room.find(FIND_CREEPS).forEach((creep) => matrix.set(creep.pos.x, creep.pos.y, 0xff));
return matrix;
}
/**
* serialize a path, traveler style. Returns a string of directions.
* @param startPos
* @param path
* @param color
* @returns {string}
*/
static serializePath(startPos, path, color = "orange") {
let serializedPath = "";
let lastPosition = startPos;
this.circle(startPos, color);
for (let position of path) {
if (position.roomName === lastPosition.roomName) {
new RoomVisual(position.roomName)
.line(position, lastPosition, { color: color, lineStyle: "dashed" });
serializedPath += lastPosition.getDirectionTo(position);
}
lastPosition = position;
}
return serializedPath;
}
/**
* returns a position at a direction relative to origin
* @param origin
* @param direction
* @returns {RoomPosition}
*/
static positionAtDirection(origin, direction) {
let offsetX = [0, 0, 1, 1, 1, 0, -1, -1, -1];
let offsetY = [0, -1, -1, 0, 1, 1, 1, 0, -1];
let x = origin.x + offsetX[direction];
let y = origin.y + offsetY[direction];
if (x > 49 || x < 0 || y > 49 || y < 0) {
return;
}
return new RoomPosition(x, y, origin.roomName);
}
/**
* convert room avoidance memory from the old pattern to the one currently used
* @param cleanup
*/
static patchMemory(cleanup = false) {
if (!Memory.empire) {
return;
}
if (!Memory.empire.hostileRooms) {
return;
}
let count = 0;
for (let roomName in Memory.empire.hostileRooms) {
if (Memory.empire.hostileRooms[roomName]) {
if (!Memory.rooms[roomName]) {
Memory.rooms[roomName] = {};
}
Memory.rooms[roomName].avoid = 1;
count++;
}
if (cleanup) {
delete Memory.empire.hostileRooms[roomName];
}
}
if (cleanup) {
delete Memory.empire.hostileRooms;
}
console.log(`TRAVELER: room avoidance data patched for ${count} rooms`);
}
static deserializeState(travelData, destination) {
let state = {};
if (travelData.state) {
state.lastCoord = { x: travelData.state[STATE_PREV_X], y: travelData.state[STATE_PREV_Y] };
state.cpu = travelData.state[STATE_CPU];
state.stuckCount = travelData.state[STATE_STUCK];
state.destination = new RoomPosition(travelData.state[STATE_DEST_X], travelData.state[STATE_DEST_Y], travelData.state[STATE_DEST_ROOMNAME]);
}
else {
state.cpu = 0;
state.destination = destination;
}
return state;
}
static serializeState(creep, destination, state, travelData) {
travelData.state = [creep.pos.x, creep.pos.y, state.stuckCount, state.cpu, destination.x, destination.y,
destination.roomName];
}
static isStuck(creep, state) {
let stuck = false;
if (state.lastCoord !== undefined) {
if (this.sameCoord(creep.pos, state.lastCoord)) {
// didn't move
stuck = true;
}
else if (this.isExit(creep.pos) && this.isExit(state.lastCoord)) {
// moved against exit
stuck = true;
}
}
return stuck;
}
}
Traveler.structureMatrixCache = {};
Traveler.creepMatrixCache = {};
exports.Traveler = Traveler;
// this might be higher than you wish, setting it lower is a great way to diagnose creep behavior issues. When creeps
// need to repath to often or they aren't finding valid paths, it can sometimes point to problems elsewhere in your code
const REPORT_CPU_THRESHOLD = 1000;
const DEFAULT_MAXOPS = 20000;
const DEFAULT_STUCK_VALUE = 2;
const STATE_PREV_X = 0;
const STATE_PREV_Y = 1;
const STATE_STUCK = 2;
const STATE_CPU = 3;
const STATE_DEST_X = 4;
const STATE_DEST_Y = 5;
const STATE_DEST_ROOMNAME = 6;
// assigns a function to Creep.prototype: creep.travelTo(destination)
Creep.prototype.travelTo = function (destination, options) {
return Traveler.travelTo(this, destination, options);
};