diff --git a/spec/engine/processor/intents/spawns/bornCreepSpec.js b/spec/engine/processor/intents/spawns/bornCreepSpec.js new file mode 100644 index 00000000..6a3a01e2 --- /dev/null +++ b/spec/engine/processor/intents/spawns/bornCreepSpec.js @@ -0,0 +1,219 @@ +const _ = require('lodash'), + utils = require('../../../../../src/utils'), + driver = utils.getDriver(), + C = driver.constants, + movement = require('../../../../../src/processor/intents/movement'), + bornCreep = require('../../../../../src/processor/intents/spawns/_born-creep'), + roomsEnv = require('../../../../helpers/mocks/rooms'), + creepsEnv = require('../../../../helpers/mocks/creeps'), + spawnsEnv = require('../../../../helpers/mocks/spawns'), + users = require('../../../../helpers/mocks/users'), + intents = require('../../../../helpers/mocks/intents'), + common = require('../../../../helpers/mocks/common'); + +const OWNER = users.defaultId; +const HOSTILE = 'hostile-user-id'; +const SPAWN_X = 16; +const SPAWN_Y = 1; + +function neighbor(direction) { + const [dx, dy] = utils.getOffsetsByDirection(direction); + return { x: SPAWN_X + dx, y: SPAWN_Y + dy }; +} + +function createSpawningCreep(spawn, data) { + return creepsEnv.createCreep('scout', _.merge({ + name: 'newborn', + user: spawn.user, + x: spawn.x, + y: spawn.y, + spawning: true + }, data)); +} + +function createHostile(direction, data) { + const pos = neighbor(direction); + return creepsEnv.createCreep('scout', _.merge({ + name: 'invader', + user: HOSTILE, + x: pos.x, + y: pos.y, + ageTime: 1000, + _ticksToLive: 1500, + store: {} + }, data)); +} + +function createFriendly(direction, data) { + const pos = neighbor(direction); + return creepsEnv.createCreep('scout', _.merge({ + name: 'blocker', + user: OWNER, + x: pos.x, + y: pos.y + }, data)); +} + +function createExtension(direction) { + const pos = neighbor(direction); + const extension = { + _id: common.generateId(), + type: 'extension', + room: 'E2S7', + user: OWNER, + x: pos.x, + y: pos.y, + hits: 1000, + hitsMax: 1000, + store: { energy: 0 } + }; + intents.scope.roomObjects[extension._id] = extension; + intents.scope.bulk.insert(extension); + return extension; +} + +describe('_born-creep', () => { + let scope, spawn, spawningCreep; + + beforeEach(() => { + scope = intents.scope; + intents.reset(); + scope.gameTime = 100; + scope.roomTerrain = roomsEnv.terrain.E2S7; + scope.eventLog = []; + scope.stats = { inc() {} }; + scope.roomController = { + type: 'controller', + user: OWNER, + level: 8, + x: 20, + y: 20, + safeMode: 0 + }; + + spawn = spawnsEnv.createSpawn({ + x: SPAWN_X, + y: SPAWN_Y, + user: OWNER, + spawning: { + name: 'newborn', + needTime: 3, + spawnTime: 103, + directions: [1, 2, 3, 4, 5, 6, 7, 8] + } + }); + spawningCreep = createSpawningCreep(spawn); + movement.init(scope.roomObjects, scope.roomTerrain); + }); + + it('spawns onto the first open preferred direction', () => { + const result = bornCreep(spawn, spawningCreep, scope); + + const expected = neighbor(1); + expect(result).toBe(true); + expect(spawningCreep.spawning).toBe(false); + expect(spawningCreep.x).toBe(expected.x); + expect(spawningCreep.y).toBe(expected.y); + }); + + it('delays birth when friendly creeps block all preferred exits', () => { + [1, 2, 3, 4, 5, 6, 7, 8].forEach(d => createFriendly(d, { name: 'friend' + d })); + movement.init(scope.roomObjects, scope.roomTerrain); + + const result = bornCreep(spawn, spawningCreep, scope); + + expect(result).toBe(false); + expect(spawningCreep.spawning).toBe(true); + expect(spawningCreep.x).toBe(SPAWN_X); + expect(spawningCreep.y).toBe(SPAWN_Y); + }); + + it('spawnstomps a hostile creep when fully surrounded', () => { + const hostiles = [1, 2, 3, 4, 5, 6, 7, 8].map(d => + createHostile(d, { name: 'invader' + d })); + movement.init(scope.roomObjects, scope.roomTerrain); + + const result = bornCreep(spawn, spawningCreep, scope); + + const stomped = hostiles[0]; + expect(result).toBe(true); + expect(spawningCreep.spawning).toBe(false); + expect(spawningCreep.x).toBe(stomped.x); + expect(spawningCreep.y).toBe(stomped.y); + expect(scope.roomObjects[stomped._id]).toBeUndefined(); + }); + + it('does not spawnstomp when a non-preferred direction is open', () => { + spawn.spawning.directions = [1]; + const hostile = createHostile(1); + movement.init(scope.roomObjects, scope.roomTerrain); + + const result = bornCreep(spawn, spawningCreep, scope); + + expect(result).toBe(false); + expect(spawningCreep.spawning).toBe(true); + expect(scope.roomObjects[hostile._id]).toBe(hostile); + }); + + it('under safemode ignores hostile creeps and uses the preferred direction', () => { + scope.roomController.safeMode = scope.gameTime + 5000; + spawn.spawning.directions = [1]; + const hostile = createHostile(1); + movement.init(scope.roomObjects, scope.roomTerrain); + + const result = bornCreep(spawn, spawningCreep, scope); + + const expected = neighbor(1); + expect(result).toBe(true); + expect(spawningCreep.spawning).toBe(false); + expect(spawningCreep.x).toBe(expected.x); + expect(spawningCreep.y).toBe(expected.y); + expect(scope.roomObjects[hostile._id]).toBe(hostile); + }); + + it('under safemode does not spawnstomp when surrounded by hostiles', () => { + scope.roomController.safeMode = scope.gameTime + 5000; + const hostiles = [1, 2, 3, 4, 5, 6, 7, 8].map(d => + createHostile(d, { name: 'invader' + d })); + movement.init(scope.roomObjects, scope.roomTerrain); + + const result = bornCreep(spawn, spawningCreep, scope); + + const expected = neighbor(1); + expect(result).toBe(true); + expect(spawningCreep.spawning).toBe(false); + expect(spawningCreep.x).toBe(expected.x); + expect(spawningCreep.y).toBe(expected.y); + hostiles.forEach(hostile => { + expect(scope.roomObjects[hostile._id]).toBe(hostile); + }); + }); + + it('under safemode still treats friendly creeps as birth obstacles', () => { + scope.roomController.safeMode = scope.gameTime + 5000; + spawn.spawning.directions = [1]; + createFriendly(1); + movement.init(scope.roomObjects, scope.roomTerrain); + + const result = bornCreep(spawn, spawningCreep, scope); + + expect(result).toBe(false); + expect(spawningCreep.spawning).toBe(true); + expect(spawningCreep.x).toBe(SPAWN_X); + expect(spawningCreep.y).toBe(SPAWN_Y); + }); + + it('under safemode still treats structures as birth obstacles', () => { + scope.roomController.safeMode = scope.gameTime + 5000; + spawn.spawning.directions = [1]; + createExtension(1); + movement.init(scope.roomObjects, scope.roomTerrain); + + const result = bornCreep(spawn, spawningCreep, scope); + + expect(result).toBe(false); + expect(spawningCreep.spawning).toBe(true); + expect(spawningCreep.x).toBe(SPAWN_X); + expect(spawningCreep.y).toBe(SPAWN_Y); + }); +}); diff --git a/spec/helpers/mocks/spawns.js b/spec/helpers/mocks/spawns.js new file mode 100644 index 00000000..aacf86a0 --- /dev/null +++ b/spec/helpers/mocks/spawns.js @@ -0,0 +1,29 @@ +const _ = require('lodash'), + intents = require('./intents'), + common = require('./common'), + users = require('./users'); + +const commonData = { + type: 'spawn', + room: 'E2S7', + user: users.defaultId, + name: 'Spawn1', + store: { energy: 300 }, + storeCapacityResource: { energy: 300 }, + hits: 5000, + hitsMax: 5000, + spawning: null, + toString: function(){ return `[spawn #${this._id}@${this.x},${this.y}]`; } +}; + +exports.createSpawn = function(data) { + const copy = _.merge( + _.cloneDeep(commonData), + data); + if(!copy._id) { + copy._id = common.generateId(); + } + intents.scope.roomObjects[copy._id] = copy; + intents.scope.bulk.insert(copy); + return copy; +}; diff --git a/src/processor/intents/movement.js b/src/processor/intents/movement.js index caf97029..dfcdd7f0 100644 --- a/src/processor/intents/movement.js +++ b/src/processor/intents/movement.js @@ -13,31 +13,6 @@ function canMove(object) { (!!object._pulled || !object._oldFatigue && _.some(object.body, i => i.hits > 0 && i.type == C.MOVE)); } -function checkObstacleAtXY(x,y,object, roomIsInSafeMode) { - var hasObstacle = false, hasRoad = false; - _.forEach(roomObjects, (i) => { - if (i.x != x || i.y != y) { - return; - } - if ((i.type == 'creep' || i.type == 'powerCreep') && !objects[i._id] && (!roomIsInSafeMode || roomIsInSafeMode != object.user || roomIsInSafeMode == object.user && object.user == i.user) || - i.type != 'creep' && i.type != 'powerCreep' && _.contains(C.OBSTACLE_OBJECT_TYPES, i.type) || - i.type == 'rampart' && !i.isPublic && i.user != object.user || - i.type == 'constructionSite' && i.user == object.user && _.contains(C.OBSTACLE_OBJECT_TYPES, - i.structureType)) { - hasObstacle = true; - return false; - } - if(i.type == 'road') { - hasRoad = true; - } - }); - if(hasObstacle) { - return true; - } - return utils.checkTerrain(roomTerrain, x, y, C.TERRAIN_MASK_WALL) && !hasRoad; - -} - function calcResourcesWeight(creep) { var totalCarry = _.sum(creep.store), weight = 0; for(var i = creep.body.length-1; i >= 0; i--) { @@ -180,7 +155,7 @@ exports.check = function(roomIsInSafeMode) { } } - if(!canMove(object) || !!checkObstacleAtXY(x,y, object, roomIsInSafeMode)) { + if(!canMove(object) || !!utils.checkObstacleAtXY(x, y, object, roomIsInSafeMode, roomObjects, roomTerrain, objects)) { removeFromMatrix(i); } } diff --git a/src/processor/intents/spawns/_born-creep.js b/src/processor/intents/spawns/_born-creep.js index 002a05ed..150c04e2 100644 --- a/src/processor/intents/spawns/_born-creep.js +++ b/src/processor/intents/spawns/_born-creep.js @@ -1,18 +1,15 @@ var _ = require('lodash'), utils = require('../../../utils'), - driver = utils.getDriver(), - C = driver.constants, movement = require('../movement'); module.exports = function(spawn, creep, scope) { - const {roomObjects, roomTerrain, bulk} = scope; + const {roomObjects, roomTerrain, bulk, roomController, gameTime} = scope; + + const roomIsInSafeMode = roomController && roomController.safeMode > gameTime ? roomController.user : false; + const mySafeMode = roomIsInSafeMode && roomIsInSafeMode == spawn.user; var newX, newY, isOccupied, hostileOccupied; - var checkObstacleFn = (i) => (i.x == newX && i.y == newY) && ( - _.contains(C.OBSTACLE_OBJECT_TYPES, i.type) || // just unwalkable - (i.type == 'constructionSite' && _.contains(C.OBSTACLE_OBJECT_TYPES, i.structureType)) // unwalkable site - ); var directions = [1,2,3,4,5,6,7,8]; if(spawn.spawning && spawn.spawning.directions) { @@ -25,16 +22,15 @@ module.exports = function(spawn, creep, scope) { newX = spawn.x + dx; newY = spawn.y + dy; - isOccupied = _.any(roomObjects, checkObstacleFn) || - movement.isTileBusy(newX, newY) || - (utils.checkTerrain(roomTerrain, newX, newY, C.TERRAIN_MASK_WALL) && !_.any(roomObjects, {type: 'road', x: newX, y: newY})); + isOccupied = utils.checkObstacleAtXY(newX, newY, spawn, roomIsInSafeMode, roomObjects, roomTerrain) || + movement.isTileBusy(newX, newY); if (!isOccupied) { break; } - // remember the first direction where we found a hostile creep - if(!hostileOccupied) { + // remember the first direction where we found a hostile creep (not under safemode) + if(!mySafeMode && !hostileOccupied) { hostileOccupied = _.find(roomObjects, i => i.x == newX && i.y == newY && i.type == 'creep' && i.user != spawn.user); } } @@ -57,8 +53,7 @@ module.exports = function(spawn, creep, scope) { newX = spawn.x + dx; newY = spawn.y + dy; - isOccupied = _.any(roomObjects, checkObstacleFn) || - utils.checkTerrain(roomTerrain, newX, newY, C.TERRAIN_MASK_WALL) || + isOccupied = utils.checkObstacleAtXY(newX, newY, spawn, roomIsInSafeMode, roomObjects, roomTerrain) || movement.isTileBusy(newX, newY); if (!isOccupied) { diff --git a/src/utils.js b/src/utils.js index 8740dca7..a5e7f10b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -335,6 +335,31 @@ exports.checkTerrain = function(terrain, x, y, mask) { return (code & mask) > 0; }; +exports.checkObstacleAtXY = function(x, y, object, roomIsInSafeMode, roomObjects, roomTerrain, movingObjects) { + movingObjects = movingObjects || {}; + var hasObstacle = false, hasRoad = false; + _.forEach(roomObjects, (i) => { + if (i.x != x || i.y != y) { + return; + } + if ((i.type == 'creep' || i.type == 'powerCreep') && !movingObjects[i._id] && (!roomIsInSafeMode || roomIsInSafeMode != object.user || roomIsInSafeMode == object.user && object.user == i.user) || + i.type != 'creep' && i.type != 'powerCreep' && _.contains(C.OBSTACLE_OBJECT_TYPES, i.type) || + i.type == 'rampart' && !i.isPublic && i.user != object.user || + i.type == 'constructionSite' && i.user == object.user && _.contains(C.OBSTACLE_OBJECT_TYPES, + i.structureType)) { + hasObstacle = true; + return false; + } + if(i.type == 'road') { + hasRoad = true; + } + }); + if(hasObstacle) { + return true; + } + return exports.checkTerrain(roomTerrain, x, y, C.TERRAIN_MASK_WALL) && !hasRoad; +}; + exports.checkControllerAvailability = function(type, roomObjects, roomController, offset) { var rcl = 0;