├── .screepsrc ├── _config.yml ├── utils ├── setupDockerComposeContainer.sh ├── continueTestServer.sh ├── followLogs.js ├── testConfig.js └── grafana.js ├── .gitattributes ├── src ├── .gitignore ├── prototype_structureStorage.js ├── prototype_structureController.js ├── pixel.js ├── role_questplayer.js ├── prototype_string.js ├── prototype_structure.js ├── role_builder.js ├── role_signer.js ├── role_claimer.js ├── role_defendmelee.js ├── role_nextroomerattack.js ├── role_defender.js ├── helpers.js ├── role_powerdefender.js ├── role_extractor.js ├── prototype_creep_squad.js ├── role_watcher.js ├── logging.js ├── prototype_roomPosition_structures.js ├── role_repairer.js ├── prototype_creep_mineral.js ├── screepsplus.js ├── role_powerattacker.js ├── prototype_creep_harvest.js ├── role_squadheal.js ├── quests_player.js ├── role_defendranged.js ├── role_quester.js ├── role_powerhealer.js ├── require.js ├── role_towerdrainer.js ├── role_atkeeper.js ├── role_storagefiller.js ├── role_squadsiege.js ├── helper_findMyRooms.js ├── role_autoattackmelee.js ├── role_powertransporter.js ├── prototype_creep_heal.js ├── role_atkeepermelee.js ├── role_upgrader.js ├── role_structurer.js ├── role_attackunreserve.js ├── prototype_room.js ├── brain_memory_market.js ├── role_scout.js ├── utils.js ├── prototype_creep_clean.js ├── prototype_room_defense.js ├── brain_main.js ├── prototype_room_controller.js ├── quests_host.js ├── main.js ├── role_universal.js ├── brain_squadmanager.js ├── prototype_room_find.js ├── role_reserver.js ├── prototype_room_memory.js └── brain_stats.js ├── .env.example ├── doc ├── Grafana.png ├── visualizer.png ├── roomExample.png ├── discord-logo-blue.png ├── roomsDatasVisual.png ├── Bot.md ├── BaseBuilding.md ├── NPC.md ├── Testing.md ├── Quests.md ├── Mineral.md ├── Visualization.md ├── CodeBase.md ├── API.md ├── Manual.md ├── Diplomacy.md └── Design.md ├── .worlddriven.ini ├── .tern-project ├── account.screeps.com.js.sample ├── account_local.screeps.com.js.sample ├── .screepsrc.example ├── .drone.yml ├── .gitignore ├── test-files └── mods.json ├── CONTRIBUTING.md ├── .circleci └── config.yml ├── config_local.js.example ├── docker-compose.yml ├── docker-compose-setup.yml ├── CODE_OF_CONDUCT.md ├── README.md ├── test └── test_setup.js └── package.json /.screepsrc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-time-machine -------------------------------------------------------------------------------- /utils/setupDockerComposeContainer.sh: -------------------------------------------------------------------------------- 1 | npm install 2 | npm test -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.jpg binary 3 | *.png binary 4 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | config_local.js 2 | main_local.js 3 | friends.js 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | export STEAM_API_KEY=YOUR_KEY_HERE 2 | export STEAM_ID=YOUR_ID_HERE -------------------------------------------------------------------------------- /doc/Grafana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TooAngel/screeps/HEAD/doc/Grafana.png -------------------------------------------------------------------------------- /doc/visualizer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TooAngel/screeps/HEAD/doc/visualizer.png -------------------------------------------------------------------------------- /doc/roomExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TooAngel/screeps/HEAD/doc/roomExample.png -------------------------------------------------------------------------------- /doc/discord-logo-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TooAngel/screeps/HEAD/doc/discord-logo-blue.png -------------------------------------------------------------------------------- /doc/roomsDatasVisual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TooAngel/screeps/HEAD/doc/roomsDatasVisual.png -------------------------------------------------------------------------------- /utils/continueTestServer.sh: -------------------------------------------------------------------------------- 1 | npm install 2 | cd tmp-test-server/ 3 | ../node_modules/.bin/screeps start -------------------------------------------------------------------------------- /.worlddriven.ini: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | baseMergeTimeInHours = 168 3 | perCommitTimeInHours = 0 4 | merge_method = squash 5 | -------------------------------------------------------------------------------- /src/prototype_structureStorage.js: -------------------------------------------------------------------------------- 1 | StructureStorage.prototype.isLow = function() { 2 | return this.store.energy < config.storage.lowValue; 3 | }; 4 | -------------------------------------------------------------------------------- /.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaVersion": 6, 3 | "libs": [], 4 | "loadEagerly": [ 5 | "ScreepsAutocomplete/**/*.js" 6 | ], 7 | "plugins": {} 8 | } 9 | -------------------------------------------------------------------------------- /account.screeps.com.js.sample: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** screeps.com account information */ 3 | email: 'your-email@here', password: 'your-secret' 4 | }; -------------------------------------------------------------------------------- /account_local.screeps.com.js.sample: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | email: 'W8N1', 3 | password: 'tooangel', 4 | http: true, 5 | host: '192.168.1.120', 6 | port: 21025, 7 | branch: 't1598603865459', 8 | }; 9 | -------------------------------------------------------------------------------- /src/prototype_structureController.js: -------------------------------------------------------------------------------- 1 | StructureController.prototype.isAboutToDowngrade = function() { 2 | return this.ticksToDowngrade < (CONTROLLER_DOWNGRADE[this.level] * config.controller.aboutToDowngradePercent / 100); 3 | }; 4 | -------------------------------------------------------------------------------- /.screepsrc.example: -------------------------------------------------------------------------------- 1 | ;If you launch the server without running the local Steam client, 2 | ;then the Steam Web API key is required for authenticating users. 3 | ;It can be obtained on this page: http://steamcommunity.com/dev/apikey 4 | steam_api_key = SOMEKEY 5 | -------------------------------------------------------------------------------- /src/pixel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * generatePixel 3 | */ 4 | function generatePixel() { 5 | if (global.config.pixel.enabled) { 6 | if (typeof PIXEL !== 'undefined') { 7 | if (Game.cpu.bucket >= PIXEL_CPU_COST + global.config.pixel.minBucketAfter) { 8 | Game.cpu.generatePixel(); 9 | } 10 | } 11 | } 12 | } 13 | 14 | module.exports.generatePixel = generatePixel; 15 | -------------------------------------------------------------------------------- /src/role_questplayer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * Solves quests 5 | */ 6 | 7 | roles.questplayer = {}; 8 | roles.questplayer.settings = { 9 | layoutString: 'MWMC', 10 | maxLayoutAmount: 5, 11 | }; 12 | 13 | roles.questplayer.action = function(creep) { 14 | creep.setNextSpawn(); 15 | creep.spawnReplacement(); 16 | creep.log(`activeQuest: ${global.data.activeQuest}`); 17 | creep.moveRandom(); 18 | return true; 19 | }; 20 | -------------------------------------------------------------------------------- /src/prototype_string.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable no-extend-native */ 4 | 5 | String.prototype.rightPad = function(padString, length) { 6 | let str = this; 7 | while (str.length < length) { 8 | str = str + padString; 9 | } 10 | return str; 11 | }; 12 | 13 | String.prototype.leftPad = function(padString, length) { 14 | let str = this; 15 | while (str.length < length) { 16 | str = padString + str; 17 | } 18 | return str; 19 | }; 20 | -------------------------------------------------------------------------------- /src/prototype_structure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // TODO Do we need this? 4 | 5 | /** 6 | * Provides structure memory. 7 | */ 8 | Object.defineProperty(Structure.prototype, 'memory', { 9 | get: function() { 10 | if (Memory.structures === undefined) { 11 | Memory.structures = {}; 12 | } 13 | if (Memory.structures[this.id] === undefined) { 14 | Memory.structures[this.id] = {}; 15 | } 16 | return Memory.structures[this.id]; 17 | }, 18 | set: function(v) { 19 | _.set(Memory, 'structures.' + this.id, v); 20 | }, 21 | configurable: true, 22 | enumerable: false, 23 | }); 24 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | name: drone testing pipeline 3 | type: docker 4 | 5 | platform: 6 | os: linux 7 | arch: amd64 8 | 9 | clone: 10 | git: ## fix maybe not needed 11 | image: plugins/git 12 | pull: true 13 | depath: 50 14 | tags: true 15 | 16 | steps: 17 | - name: test 18 | image: node:12 19 | pull: always 20 | commands: 21 | - git --version 22 | - npm install 23 | - npm run test-no-server 24 | volumes: 25 | - name: cache 26 | path: /tmp/cache 27 | - name: artifacts 28 | path: /node_modules 29 | 30 | trigger: 31 | event: 32 | - push -------------------------------------------------------------------------------- /utils/followLogs.js: -------------------------------------------------------------------------------- 1 | const {logConsole, followLog, setHostname} = require('./testHelpers'); 2 | const {playerRoom, rooms} = require('./testConfig'); 3 | 4 | /** 5 | * main 6 | */ 7 | async function main() { 8 | await new Promise((resolve) => setTimeout(resolve, 5000)); 9 | if (process.argv.length > 2) { 10 | setHostname(process.argv[2]); 11 | } 12 | let onlyPlayerRoom; 13 | if (process.argv.length > 3) { 14 | onlyPlayerRoom = playerRoom; 15 | } 16 | for (let i=0; i<1000; i++) { 17 | console.log(i); 18 | try { 19 | await followLog(rooms, logConsole, undefined, onlyPlayerRoom); 20 | } catch (e) { 21 | console.log(e); 22 | await new Promise((resolve) => setTimeout(resolve, 1000)); 23 | } 24 | } 25 | } 26 | main(); 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ 3 | dist/ 4 | 5 | ## used for some private servers, dist/**.js concated into build 6 | # make sure requrie.js is not concated and main.js is concated at last 7 | build/ 8 | 9 | ## steam sync file 10 | .sync 11 | 12 | ## screeps.com account.email and account.password 13 | account.screeps.com.js 14 | 15 | .localSync.js 16 | 17 | ## Autocomplete 18 | ScreepsAutocomplete/ 19 | steam_appid.txt 20 | 21 | .screepsrctest-server/logs/ 22 | test-server/logs/ 23 | tests/logs/ 24 | tests/db.json 25 | tmp-test-server/ 26 | server/ 27 | account_local.screeps.com.js 28 | current-world/ 29 | tmp-test-server-database/ 30 | 31 | ## ide 32 | target/ 33 | .settings/ 34 | .idea/ 35 | **/*.iml 36 | 37 | ## no ts 38 | tsconfig.json 39 | src_ts/ 40 | .screeps.yaml 41 | -------------------------------------------------------------------------------- /test-files/mods.json: -------------------------------------------------------------------------------- 1 | { 2 | "mods": [ 3 | "../node_modules/screepsmod-auth/index.js", 4 | "../node_modules/screepsmod-admin-utils/index.js", 5 | "../node_modules/screepsmod-mongo/index.js" 6 | ], 7 | "bots": { 8 | "screeps-bot-tooangel": "../src", 9 | "screeps-bot-hivemind": "../node_modules/screeps-bot-hivemind/src", 10 | "screeps-bot-choreographer": "../node_modules/screeps-bot-choreographer/dist", 11 | "screeps-bot-international": "../node_modules/@the-international-screeps-bot/the-international-open-source/dist", 12 | "screeps-bot-kasamibot": "../node_modules/screeps-bot-kasamibot/dist", 13 | "screeps-bot-overmind": "../node_modules/screeps-bot-overmind/src", 14 | "screeps-bot-tom": "../node_modules/tom-screeps-bot/src/" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | All kind of contribution is welcome, issues, contact via channels, pull requests. 4 | 5 | This repository uses 6 | [world driven](https://www.worlddriven.org/) 7 | for automatic merging. 8 | 9 | Pull requests needs to be send to the master, all checks needs to be good green. 10 | The automatic merge is time based depending on github review workflow outcome. 11 | 12 | - `Request changes` reduces the outcome. 13 | - `Approve` increases the outcome 14 | 15 | The weight of each review if based on the number of commits of the reviewer. 16 | If the outcome is negative the merge will not happen. Higher positive outcome 17 | will decrease the time to merge. 18 | 19 | Start joining this project, be part of it, if you like - anyway have fun ;-) 20 | -------------------------------------------------------------------------------- /src/role_builder.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * builder builds up construction sites 5 | * 6 | * Moves to the construction sites, does random walk to prevent traffic jam 7 | * builds up the structure 8 | */ 9 | 10 | roles.builder = {}; 11 | roles.builder.boostActions = ['build', 'capacity']; 12 | 13 | roles.builder.settings = { 14 | layoutString: 'MCW', 15 | amount: [2, 1, 1], 16 | maxLayoutAmount: 20, 17 | }; 18 | 19 | roles.builder.action = function(creep) { 20 | const methods = [Creep.getEnergy]; 21 | 22 | methods.push(Creep.constructTask); 23 | if (creep.room.memory.misplacedSpawn) { 24 | methods.push(Creep.transferEnergy); 25 | methods.push(Creep.repairStructure); 26 | } 27 | methods.push(Creep.upgradeControllerTask); 28 | 29 | return Creep.execute(creep, methods); 30 | }; 31 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | deploy: 4 | docker: 5 | - image: node:12 6 | steps: 7 | - checkout 8 | - run: npm install 9 | - run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 10 | - run: npm publish || true 11 | - run: npm run deploy 12 | - run: npm run respawner 13 | test: 14 | docker: 15 | - image: node:12 16 | - image: mongo:latest 17 | - image: redis:latest 18 | steps: 19 | - checkout 20 | - run: npm install 21 | - run: 22 | command: npm run test 23 | no_output_timeout: 2h 24 | workflows: 25 | version: 2 26 | test_and_deploy: 27 | jobs: 28 | - test 29 | - deploy: 30 | requires: 31 | - test 32 | filters: 33 | branches: 34 | only: master 35 | -------------------------------------------------------------------------------- /doc/Bot.md: -------------------------------------------------------------------------------- 1 | # TooAngel bot on a private server 2 | 3 | You feel lonely, already dominating the default bots or just want to have some more action on your server, then you are at the right place. 4 | 5 | ## Installation 6 | 7 | Follow the instructions on [Steam](http://steamcommunity.com/sharedfiles/filedetails/?id=800902233) or 8 | 9 | - Launch your `Dedicated Server` via steam 10 | - Click on `MODS` 11 | - `Browse Steam Workshop` 12 | - Search for `screeps-bot-tooangel` and `subscribe` 13 | - Restart the server 14 | - And execute in the `CLI` `bots.spawn('screeps-bot-tooangel', 'W7N4')` (or replace the room name with something else) 15 | 16 | ### Alternatively 17 | 18 | - Add 19 | ``` 20 | "bots": { 21 | "screeps-bot-tooangel": "node_modules/screeps-bot-tooangel/src" 22 | } 23 | ``` 24 | to your `mods.json` 25 | 26 | ## Interaction 27 | 28 | For interaction details head of to [TooAngel NPC](NPC.md) -------------------------------------------------------------------------------- /src/role_signer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * signer is used to sign controller in external rooms 5 | * 6 | * Moves to the controller and signs 7 | */ 8 | 9 | roles.signer = {}; 10 | 11 | roles.signer.settings = { 12 | layoutString: 'M', 13 | maxLayoutAmount: 1, 14 | }; 15 | 16 | roles.signer.action = function(creep) { 17 | const returnCode = creep.signController(creep.room.controller, creep.memory.signText || config.info.signText); 18 | if (returnCode === OK) { 19 | // if (creep.memory.nextTarget) { 20 | // creep.memory.signText = creep.memory.nextTarget.signText || creep.memory.signText; 21 | // creep.memory.routing = creep.memory.nextTarget.routing; 22 | // creep.memory.nextTarget = creep.memory.nextTarget.nextTarget; 23 | // } else { 24 | creep.suicide(); 25 | // } 26 | return true; 27 | } else { 28 | creep.log(returnCode); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/role_claimer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * claimer is called when the number of rooms is < possible rooms 5 | * 6 | * targetRoom and targetId is set so the creep just follows the path 7 | * Claims the controller and room revive will drop in. 8 | */ 9 | 10 | roles.claimer = {}; 11 | 12 | roles.claimer.settings = { 13 | layoutString: 'MK', 14 | maxLayoutAmount: 1, 15 | }; 16 | 17 | roles.claimer.action = function(creep) { 18 | creep.creepLog('New claimer, in room, claiming'); 19 | // TODO just added the targetId to the creep, I hope it works 20 | // const returnCodeMove = creep.moveTo(creep.room.controller.pos); 21 | // console.log(`Move returnCode ${returnCodeMove}`); 22 | const returnCode = creep.claimController(creep.room.controller); 23 | if (returnCode === OK) { 24 | creep.creepLog('New claimer, in room, claimed'); 25 | creep.suicide(); 26 | } 27 | return true; 28 | }; 29 | -------------------------------------------------------------------------------- /config_local.js.example: -------------------------------------------------------------------------------- 1 | // Local overrides to config, modify this instead of global config. 2 | //--- Uncomment screepsplusToken for share data on the shared dashboard on screepspl.us 3 | //--- You can access the data with username `tooangels` and password `tooSecretPassword` 4 | //config.stats.screepsPlusToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRvb2FuZ2VscyIsImlhdCI6MTQ4MzU2MTU3OSwiYXVkIjoic2NyZWVwc3BsLnVzIiwiaXNzIjoic2NyZWVwc3BsLnVzIn0.NhobT7Jg8bOAg-MYqrYsgeMgXEVXGVYG9s3G9Qpfm-o'; 5 | config.stats.enabled = true; 6 | config.stats.summary = true; 7 | config.profiler.enabled = false; 8 | 9 | //--- Enables a summary with all rooms for every tick 10 | //global.config.tickSummary.bucket = true; 11 | //global.config.tickSummary.gcl = true; 12 | //global.config.tickSummary.room = true; 13 | //global.config.tickSummary.separator = true; // Adds a seperatorline after each tick 14 | 15 | //--- Enables pixel generation for live server. 16 | //global.config.pixel.enabled = true; 17 | -------------------------------------------------------------------------------- /doc/BaseBuilding.md: -------------------------------------------------------------------------------- 1 | # Room 2 | 3 | ## Setup 4 | 5 | Positions: 6 | - `upgrader` creep next to the `controller` 7 | - `storage` structure next to the `upgrader` 8 | - `filler` creep next to the `storage` 9 | - `pathStart` position next to the `storage` 10 | 11 | 12 | From `pathStart` all (sources, controller, mineral, mid of each exit) paths 13 | are calculated and saved. The longest path is used to place structures (spawn, 14 | extension, lab, observer, terminal, tower) next to it. Next to `filler` a link, 15 | tower and power_spawn is located. `Link`s are placed next to the sources and at 16 | the paths to the exits. Layers of walls are placed at the exits, positions 17 | within the precalculated paths are replaced by ramparts. 18 | 19 | ## Pathing 20 | 21 | Paths are precalculated and cached and reused by most of the creeps. 22 | 23 | Swamps are ignored because roads will be built automatically over time. 24 | The creeps only move on the precalculated paths (to reduce complexity). Instead, blockers are recognized and `structurer` are sent to destroy the structure, `carry` creeps try it as well. -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | redis: 3 | image: redis:latest 4 | ports: 5 | - 6379:6379 6 | command: redis-server --loglevel warning 7 | # Enable memory overcommit to prevent save/replication failures 8 | # If this doesn't work, run: sudo sysctl vm.overcommit_memory=1 9 | sysctls: 10 | - net.core.somaxconn=511 11 | # Uncomment if you have permission issues with sysctls: 12 | # privileged: true 13 | mongo: 14 | image: mongo 15 | volumes: 16 | - ./tmp-test-server-database/:/data/db 17 | ports: 18 | - 27017:27017 19 | command: mongod --quiet --logpath /dev/null 20 | server: 21 | image: node:20 22 | working_dir: /mnt 23 | volumes: 24 | - ./:/mnt 25 | command: ./utils/continueTestServer.sh 26 | environment: 27 | - STEAM_API_KEY 28 | - STEAM_ID 29 | - MONGO_HOST=mongo 30 | - REDIS_HOST=redis 31 | env_file: 32 | - .env 33 | depends_on: 34 | - redis 35 | - mongo 36 | #network_mode: "host" 37 | ports: 38 | - 21025:21025 39 | - 21026:21026 -------------------------------------------------------------------------------- /src/role_defendmelee.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * defendmelee is called after 'threshold' when a room is attacked 5 | * 6 | * Tries to fight against the hostile creeps 7 | */ 8 | 9 | roles.defendmelee = {}; 10 | 11 | roles.defendmelee.settings = { 12 | layoutString: 'MA', 13 | amount: [1, 1], 14 | fillTough: true, 15 | }; 16 | 17 | roles.defendmelee.action = function(creep) { 18 | const hostile = creep.findClosestEnemy(); 19 | if (hostile === null) { 20 | if (creep.room.memory.attackTimer % 3 === 0) { 21 | return Creep.recycleCreep(creep); 22 | } else { 23 | return true; 24 | } 25 | } 26 | const search = PathFinder.search( 27 | creep.pos, 28 | hostile.pos, { 29 | roomCallback: creep.room.getCostMatrixCallback(hostile.pos), 30 | }); 31 | if (config.visualizer.enabled && config.visualizer.showPathSearches) { 32 | visualizer.showSearch(search); 33 | } 34 | const direction = creep.pos.getDirectionTo(search.path[0]); 35 | creep.moveCreep(search.path[0], RoomPosition.oppositeDirection(direction)); 36 | creep.move(direction); 37 | creep.attack(hostile); 38 | }; 39 | -------------------------------------------------------------------------------- /src/role_nextroomerattack.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {cleanUpDyingCreep} = require('./brain_main'); 4 | 5 | /* 6 | * nextroomerattack is called if the route to the room to revive is blocked 7 | * 8 | * Attacks hostile everything 9 | */ 10 | 11 | roles.nextroomerattack = {}; 12 | 13 | roles.nextroomerattack.settings = { 14 | layoutString: 'MA', 15 | amount: [5, 5], 16 | fillTough: true, 17 | }; 18 | 19 | roles.nextroomerattack.died = function(name) { 20 | cleanUpDyingCreep(name); 21 | }; 22 | 23 | roles.nextroomerattack.action = function(creep) { 24 | if (!creep.memory.notified) { 25 | creep.log('Attacking'); 26 | Game.notify(Game.time + ' ' + creep.room.name + ' Attacking'); 27 | creep.memory.notified = true; 28 | } 29 | const spawn = creep.pos.findClosestByRangeHostileSpawn(); 30 | 31 | if (spawn === null) { 32 | const hostileCreep = creep.findClosestEnemy(); 33 | if (hostileCreep !== null) { 34 | creep.moveTo(hostileCreep); 35 | creep.attack(hostileCreep); 36 | } 37 | return true; 38 | } 39 | const path = creep.pos.findPathTo(spawn, { 40 | ignoreDestructibleStructures: true, 41 | }); 42 | creep.attack(spawn); 43 | creep.moveByPath(path); 44 | return true; 45 | }; 46 | -------------------------------------------------------------------------------- /src/role_defender.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * Called to defend external rooms 5 | * 6 | * Fights against hostile creeps 7 | */ 8 | 9 | roles.defender = {}; 10 | roles.defender.boostActions = ['rangedAttack', 'heal']; 11 | 12 | roles.defender.settings = { 13 | param: ['controller.level'], 14 | layoutString: 'MRH', 15 | amount: { 16 | 1: [2, 1, 1], 17 | 8: [4, 1, 1], 18 | }, 19 | // fillTough: true, 20 | }; 21 | 22 | roles.defender.preMove = function(creep) { 23 | creep.selfHeal(); 24 | const target = creep.findClosestEnemy(); 25 | if (target !== null) { 26 | creep.creepLog(`preMove foundClosestEnemy ${target}`); 27 | creep.handleDefender(); 28 | return true; 29 | } 30 | if (!creep.inMyRoom()) { 31 | let targets = creep.pos.findInRangeStructures(FIND_HOSTILE_STRUCTURES); 32 | if (targets.length === 0) { 33 | targets = creep.pos.findInRangeStructures(FIND_STRUCTURES, 1, [STRUCTURE_WALL, STRUCTURE_RAMPART]); 34 | } 35 | creep.rangeAttackOutsideOfMyRooms(targets); 36 | } 37 | }; 38 | 39 | roles.defender.action = function(creep) { 40 | if (creep.inBase() && creep.memory.reverse) { 41 | return Creep.recycleCreep(creep); 42 | } 43 | 44 | creep.selfHeal(); 45 | creep.handleDefender(); 46 | return true; 47 | }; 48 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * printRoomCostMatrix - prints the room.data.costMatrix 3 | * 4 | * @param {object} room 5 | * @return {void} 6 | */ 7 | function printRoomCostMatrix(room) { 8 | let line = ' '; 9 | for (let x = 0; x < 50; x++) { 10 | line += x.toString().padStart(3, ' '); 11 | } 12 | console.log(line); 13 | for (let y = 0; y < 50; y++) { 14 | let line = `${y.toString().padStart(3, ' ')} `; 15 | for (let x = 0; x < 50; x++) { 16 | line += room.data.costMatrix.get(x, y).toString().padStart(3, ' '); 17 | } 18 | console.log(line); 19 | } 20 | } 21 | 22 | module.exports.printRoomCostMatrix = printRoomCostMatrix; 23 | 24 | /** 25 | * printCostMatrix - prints the room.data.costMatrix 26 | * 27 | * @param {object} costMatrix 28 | * @return {void} 29 | */ 30 | function printCostMatrix(costMatrix) { 31 | let line = ' '; 32 | for (let x = 0; x < 50; x++) { 33 | line += x.toString().padStart(3, ' '); 34 | } 35 | console.log(line); 36 | for (let y = 0; y < 50; y++) { 37 | let line = `${y.toString().padStart(3, ' ')} `; 38 | for (let x = 0; x < 50; x++) { 39 | line += costMatrix.get(x, y).toString().padStart(3, ' '); 40 | } 41 | console.log(line); 42 | } 43 | } 44 | 45 | module.exports.printCostMatrix = printCostMatrix; 46 | -------------------------------------------------------------------------------- /src/role_powerdefender.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * powerdefender is called when hostile creeps show up on power harvesting 5 | * 6 | * Kills hostile creeps 7 | */ 8 | 9 | roles.powerdefender = {}; 10 | 11 | roles.powerdefender.settings = { 12 | layoutString: 'MR', 13 | maxLayoutAmount: 16, 14 | fillTough: true, 15 | }; 16 | 17 | roles.powerdefender.action = function(creep) { 18 | if (creep.hits < 200) { 19 | return false; 20 | } 21 | const hostileCreeps = creep.room.findEnemies(); 22 | if (hostileCreeps.length) { 23 | creep.moveTo(hostileCreeps[0]); 24 | creep.rangedAttack(hostileCreeps[0]); 25 | return true; 26 | } 27 | const powerBank = creep.room.findPropertyFilter(FIND_STRUCTURES, 'structureType', [STRUCTURE_POWER_BANK]); 28 | if (!powerBank.length) { 29 | const hostileCreep = creep.findClosestEnemy(); 30 | if (hostileCreep !== null) { 31 | creep.moveTo(hostileCreep); 32 | creep.rangedAttack(hostileCreep); 33 | return true; 34 | } 35 | creep.moveTo(25, 25); 36 | return false; 37 | } 38 | 39 | if (powerBank[0].hits > 100000) { 40 | creep.spawnReplacement(); 41 | } 42 | 43 | creep.setNextSpawn(); 44 | 45 | creep.moveTo(powerBank[0]); 46 | creep.rangedAttack(powerBank[0]); 47 | return true; 48 | }; 49 | -------------------------------------------------------------------------------- /src/role_extractor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * extractor gets minerals from the extractor 5 | * 6 | * Moves, harvest, brings to the terminal 7 | */ 8 | 9 | roles.extractor = {}; 10 | 11 | roles.extractor.settings = { 12 | prefixString: 'WMC', 13 | layoutString: 'MW', 14 | amount: [1, 2], 15 | maxLayoutAmount: 999, 16 | }; 17 | 18 | roles.extractor.boostActions = ['harvest']; 19 | roles.extractor.buildRoad = true; 20 | 21 | roles.extractor.preMove = function(creep, directions) { 22 | creep.preMoveExtractorSourcer(directions); 23 | }; 24 | 25 | /** 26 | * getMineral - Gets the mineral from heap data, or sets if missing 27 | * 28 | * @param {object} creep - The creep 29 | * @return {object} - The tower 30 | **/ 31 | function getMineral(creep) { 32 | if (!creep.data.mineral) { 33 | const minerals = creep.room.findMinerals(); 34 | creep.data.mineral = minerals[0].id; 35 | } 36 | return Game.getObjectById(creep.data.mineral); 37 | } 38 | 39 | roles.extractor.action = function(creep) { 40 | if (!creep.room.terminal) { 41 | creep.suicide(); 42 | return true; 43 | } 44 | const mineral = getMineral(creep); 45 | creep.harvest(mineral); 46 | for (const key in creep.store) { 47 | if (creep.store[key] === 0) { 48 | continue; 49 | } 50 | creep.transfer(creep.room.terminal, key); 51 | } 52 | return true; 53 | }; 54 | -------------------------------------------------------------------------------- /docker-compose-setup.yml: -------------------------------------------------------------------------------- 1 | services: 2 | redis: 3 | image: redis:latest 4 | ports: 5 | - 6379:6379 6 | command: redis-server --loglevel warning 7 | # Enable memory overcommit to prevent save/replication failures 8 | # If this doesn't work, run: sudo sysctl vm.overcommit_memory=1 9 | sysctls: 10 | - net.core.somaxconn=511 11 | # Uncomment if you have permission issues with sysctls: 12 | # privileged: true 13 | mongo: 14 | image: mongo 15 | volumes: 16 | - ./tmp-test-server-database/:/data/db 17 | ports: 18 | - 27017:27017 19 | command: mongod --quiet --logpath /dev/null 20 | server: 21 | image: node:20 22 | working_dir: /mnt 23 | volumes: 24 | - ./:/mnt 25 | command: ./utils/setupDockerComposeContainer.sh 26 | environment: 27 | - STEAM_API_KEY 28 | - STEAM_ID 29 | - MONGO_HOST=mongo 30 | - REDIS_HOST=redis 31 | env_file: 32 | - .env 33 | depends_on: 34 | - redis 35 | - mongo 36 | #network_mode: "host" 37 | ports: 38 | - 21025:21025 39 | - 21026:21026 40 | logs: 41 | image: node:20 42 | working_dir: /mnt 43 | volumes: 44 | - ./:/mnt 45 | command: node utils/followLogs.js localhost true 46 | environment: 47 | - STEAM_API_KEY 48 | - STEAM_ID 49 | depends_on: 50 | - server 51 | network_mode: "host" 52 | profiles: 53 | - donotstart 54 | -------------------------------------------------------------------------------- /doc/NPC.md: -------------------------------------------------------------------------------- 1 | # TooAngel NPC 2 | 3 | Welcome stranger. Nice to see you here. 4 | Let me introduce myself, I'm the TooAngel NPC a [community driven](https://www.worlddriven.org) account. If you like to get in contact with makers head over to [discord](https://discord.com/channels/860665589738635336/931237079315251280). 5 | 6 | ## Reputation 7 | 8 | From here on you can decide how to interact with the TooAngel NPC, based on your actions you can build up or decrease your reputation. 9 | 10 | I don't like: 11 | 12 | - Hostile creeps into my rooms 13 | - Destroying structures of even spawns 14 | - Sending nukes 15 | 16 | I like: 17 | 18 | - Sending resources via terminal 19 | - Solving [Quests](Quests.md) 20 | 21 | Based on the level of reputation certain actions will be performed, like: 22 | 23 | ### Negative 24 | 25 | - Be annoying in your reserved rooms 26 | - Attacking your rooms 27 | - Sending nukes 28 | 29 | ### Positive 30 | 31 | - Your creeps are seen as friends in the open areas 32 | - Sharing, instead of defending, resources like power 33 | - Open to accept quests from you 34 | 35 | ## Basic behavior 36 | 37 | - I keep a safety radius of 1 linear distance around the rooms and attack other controlled or reserved rooms 38 | - Hostile actions are retaliated 39 | - I like my rooms, if attacked or destroyed, I'll try the best to keep them, even after loosing the last spawn. 40 | 41 | ## API 42 | 43 | For communication head over to the [API](API.md) 44 | -------------------------------------------------------------------------------- /doc/Testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | ## Local Testing Environment 4 | 5 | To test the bot in a local environment: 6 | - Create a `.env` file in the project root (see `.env.example` for reference) 7 | - This enables local development and testing without affecting the live bot 8 | 9 | ## Automated Testing 10 | 11 | The following tests are automatically executed on new PRs and commits to master: 12 | 13 | ### Code Quality Checks 14 | - **ESLint**: Code style and formatting validation 15 | - **CircleCI Integration**: Continuous integration pipeline 16 | 17 | ### Functional Testing 18 | - **Private Server Testing**: Deploys bot to a test server 19 | - **Controller Upgrade Validation**: Verifies the bot can successfully upgrade room controllers within expected timeframes 20 | - **Performance Benchmarks**: Ensures the bot operates efficiently under various conditions 21 | 22 | ## Testing Commands 23 | 24 | ```bash 25 | # Setup test server with multiple bots 26 | npm run setupTestServer 27 | 28 | # Resume existing test environment 29 | docker compose up 30 | 31 | # Run local deployment 32 | npm run deployLocal 33 | 34 | # Manual testing server 35 | node utils/test.js 36 | ``` 37 | 38 | ## Test Configuration 39 | 40 | For local testing configuration, see [CodeBase.md](CodeBase.md) for details on: 41 | - Setting up `.screeps.yaml` for local server connections 42 | - Configuring `config_local.js` for testing scenarios 43 | - Using debug flags for detailed logging during tests 44 | -------------------------------------------------------------------------------- /src/prototype_creep_squad.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * initializeSquadMembership 5 | * 6 | * Registers a creep as a member of its squad. Ensures Memory.squads structure exists. 7 | * @param {string} squadType - Type of squad role (e.g., 'siege', 'heal', 'autoattackmelee') 8 | */ 9 | Creep.prototype.initializeSquadMembership = function(squadType) { 10 | if (this.memory.initialized) { 11 | return; 12 | } 13 | 14 | if (!Memory.squads) { 15 | Memory.squads = {}; 16 | } 17 | if (!Memory.squads[this.memory.squad]) { 18 | Memory.squads[this.memory.squad] = {}; 19 | } 20 | if (!Memory.squads[this.memory.squad][squadType]) { 21 | Memory.squads[this.memory.squad][squadType] = {}; 22 | } 23 | 24 | Memory.squads[this.memory.squad][squadType][this.id] = {}; 25 | this.memory.initialized = true; 26 | }; 27 | 28 | Creep.prototype.squadMove = function(squad, maxRange, moveRandom, role) { 29 | if (this.room.name === squad.moveTarget) { 30 | const nextExits = this.room.find(this.memory.routing.route[this.memory.routing.routePos].exit); 31 | if (nextExits.length < 1) { 32 | return false; 33 | } 34 | const nextExit = nextExits[Math.floor(nextExits.length / 2)]; 35 | const range = this.pos.getRangeTo(nextExit.x, nextExit.y); 36 | if (range < maxRange) { 37 | Memory.squads[this.memory.squad][role][this.id].waiting = true; 38 | if (moveRandom) { 39 | this.moveRandom(); 40 | } 41 | return true; 42 | } 43 | } 44 | return false; 45 | }; 46 | -------------------------------------------------------------------------------- /src/role_watcher.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * scout moves around to provide visibility 5 | * 6 | * Pre observer the scout moves through surrounding rooms 7 | */ 8 | // role_watcher.js 9 | roles.watcher = {}; 10 | roles.watcher.settings = { 11 | layoutString: 'M', 12 | amount: [1], 13 | maxLayoutAmount: 1, 14 | }; 15 | 16 | roles.watcher.preMove = function(creep, directions) { 17 | if (creep.hits < creep.hitsMax) { 18 | creep.memory.routing.reverse = true; 19 | if (directions) { 20 | directions.direction = directions.backwardDirection; 21 | } 22 | return false; 23 | } else { 24 | creep.memory.routing.reverse = false; 25 | } 26 | if (creep.memory.routing.targetRoom === creep.room.name) { 27 | const pos = new RoomPosition(25, 25, creep.memory.routing.targetRoom); 28 | if (creep.pos.isNearTo(pos)) { 29 | creep.memory.routing.reached = true; 30 | } 31 | } else { 32 | delete creep.memory.routing.reached; 33 | } 34 | }; 35 | 36 | roles.watcher.action = function(creep) { 37 | creep.setNextSpawn(); 38 | creep.spawnReplacement(1); 39 | const pos = new RoomPosition(25, 25, creep.memory.routing.targetRoom); 40 | const near = 4; 41 | if (creep.pos.isNearTo(pos)) { 42 | creep.moveRandomWithin(pos); 43 | let creepOfRole = creep.room.findPropertyFilter(FIND_MY_CREEPS, 'memory.role', ['watcher'], { 44 | filter: (o) => o.memory.routing.targetRoom === creep.memory.routing.targetRoom, 45 | }); 46 | if (creepOfRole.length > 1) { 47 | creepOfRole = _.sortBy(creepOfRole, (c) => c.ticksToLive); 48 | creepOfRole[0].suicide(); 49 | } 50 | } else { 51 | creep.moveToMy(pos, near); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/logging.js: -------------------------------------------------------------------------------- 1 | /** 2 | * debugLog 3 | * 4 | * @param {string} type 5 | * @param {...string} messages 6 | */ 7 | function debugLog(type, ...messages) { 8 | if (config.debug[type]) { 9 | console.log(`${Game.time} ${type.rightPad(' ', 27)} ${messages.join(' ')}`); 10 | } 11 | } 12 | 13 | module.exports.debugLog = debugLog; 14 | 15 | Room.prototype.log = function(...messages) { 16 | console.log(`${Game.time} ${this.name.rightPad(' ', 27)} ${messages.join(' ')}`); 17 | }; 18 | 19 | Room.prototype.debugLog = function(type, ...messages) { 20 | if (config.debug[type]) { 21 | this.log(type, messages); 22 | } 23 | }; 24 | 25 | RoomObject.prototype.log = function(...messages) { 26 | const name = this.name || this.structureType; 27 | console.log(`${Game.time} ${this.room.name.rightPad(' ', 6)} ${name.rightPad(' ', 20)} ${this.pos} ${messages.join(' ')}`); 28 | }; 29 | 30 | RoomPosition.prototype.log = function(...messages) { 31 | const coords = ('[' + this.x + ',' + this.y + ']').rightPad(' ', 20); 32 | console.log(`${Game.time} ${this.roomName.rightPad(' ', 6)} ${coords} ${messages.join(' ')}`); 33 | }; 34 | /* 35 | * Log creep message based on debug config 36 | * 37 | * `config.debug.creepLog.roles` and `config.debug.creepLog.rooms` define 38 | * logging on common methods 39 | * 40 | * @param messages The message to log 41 | */ 42 | Creep.prototype.creepLog = function(...messages) { 43 | if (config.debug.creepLog.roles !== '*' && config.debug.creepLog.roles.indexOf(this.memory.role) < 0) { 44 | return; 45 | } 46 | if (config.debug.creepLog.rooms !== '*' && config.debug.creepLog.rooms.indexOf(this.room.name) < 0) { 47 | return; 48 | } 49 | this.log(messages); 50 | }; 51 | -------------------------------------------------------------------------------- /src/prototype_roomPosition_structures.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | RoomPosition.prototype.setSpawn = function(path, pathI) { 4 | const posNext = path[+pathI + 1]; 5 | const pathPos = new RoomPosition(path[pathI].x, path[pathI].y, path[pathI].roomName); 6 | // TODO Check distance to other spawns 7 | const room = Game.rooms[this.roomName]; 8 | if ((room.data.positions.structure.spawn || []).length >= CONTROLLER_STRUCTURES.spawn[8]) { 9 | return false; 10 | } 11 | 12 | const directionStructure = pathPos.getDirectionTo(this.x, this.y); 13 | 14 | if (directionStructure === BOTTOM) { 15 | return true; 16 | } 17 | 18 | if (!posNext) { 19 | room.log('No posNext: ' + pathPos); 20 | return false; 21 | } 22 | 23 | const directionNext = pathPos.getDirectionTo(posNext.x, posNext.y); 24 | 25 | if (directionNext === RIGHT && directionStructure === BOTTOM_RIGHT) { 26 | return true; 27 | } 28 | 29 | if (directionNext === LEFT && directionStructure === BOTTOM_LEFT) { 30 | return true; 31 | } 32 | 33 | if (directionNext === TOP_RIGHT && directionStructure === RIGHT) { 34 | return true; 35 | } 36 | 37 | if (directionNext === TOP_LEFT && directionStructure === LEFT) { 38 | return true; 39 | } 40 | 41 | return false; 42 | }; 43 | 44 | RoomPosition.prototype.setExtension = function() { 45 | const room = Game.rooms[this.roomName]; 46 | if ((room.data.positions.structure.extension || []).length >= CONTROLLER_STRUCTURES.extension[8]) { 47 | return false; 48 | } 49 | return true; 50 | }; 51 | 52 | RoomPosition.prototype.inRamparts = function() { 53 | for (const rampart of Memory.rooms[this.roomName].walls.ramparts) { 54 | if (this.isEqualTo(rampart.x, rampart.y)) { 55 | return true; 56 | } 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /src/role_repairer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * repairer should be always present 5 | * 6 | * Repairs walls and ramparts 7 | */ 8 | 9 | // TODO get energy from links 10 | // TODO move with the CostMatrix 11 | 12 | roles.repairer = {}; 13 | 14 | roles.repairer.settings = { 15 | layoutString: 'MWC', 16 | amount: [2, 1, 1], 17 | maxLayoutAmount: 5, 18 | }; 19 | 20 | roles.repairer.boostActions = ['repair', 'capacity']; 21 | 22 | roles.repairer.preMove = function(creep) { 23 | creep.memory.routing.reached = true; 24 | if (creep.memory.routing && creep.memory.routing.targetId) { 25 | if (Game.getObjectById(creep.memory.routing.targetId) === null) { 26 | creep.creepLog('target does not exist anymore'); 27 | delete creep.memory.routing.targetId; 28 | roles.repairer.action(creep); 29 | return true; 30 | } 31 | } 32 | }; 33 | 34 | roles.repairer.action = function(creep) { 35 | if (creep.pos.roomName !== creep.memory.base) { 36 | creep.log(`Not in my base room why? ${creep.memory.routing.targetId} carry: ${JSON.stringify(creep.carry)} positions: ${JSON.stringify(creep.memory.lastPositions)}`); 37 | } 38 | creep.setNextSpawn(); 39 | creep.spawnReplacement(1); 40 | creep.pickupEnergy(); 41 | if (!creep.memory.move_wait) { 42 | creep.memory.move_wait = 0; 43 | } 44 | 45 | if (creep.memory.step <= 0) { 46 | const structures = creep.room.findDefenseStructures(); 47 | let min = WALL_HITS_MAX; 48 | if (structures.length > 0) { 49 | for (const structure of structures) { 50 | if (min > structure.hits) { 51 | min = structure.hits; 52 | } 53 | } 54 | } 55 | creep.memory.step = min; 56 | } 57 | 58 | const methods = [Creep.getEnergy]; 59 | methods.push(Creep.repairStructure); 60 | methods.push(Creep.constructTask); 61 | 62 | if (Creep.execute(creep, methods)) { 63 | return true; 64 | } 65 | return true; 66 | }; 67 | -------------------------------------------------------------------------------- /src/prototype_creep_mineral.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Check is a given lab as enough mineral for reaction 5 | * 6 | * @param {object} lab - The lab to check. 7 | * @param {string} mineralType - The mineral type to check. 8 | * @return {boolean} if the lab has enough of the mineral 9 | */ 10 | Creep.prototype.checkLabEnoughMineral = function(lab, mineralType) { 11 | if (lab.mineralAmount < LAB_REACTION_AMOUNT && !this.room.terminal.store[mineralType] && !this.carry[mineralType]) { 12 | if (config.debug.mineral) { 13 | this.log('Not enough', mineralType, 'stop reaction'); 14 | } 15 | delete this.room.memory.reaction; 16 | return false; 17 | } 18 | return true; 19 | }; 20 | 21 | Creep.prototype.getBoostParts = function() { 22 | const parts = {}; 23 | for (const part of this.body) { 24 | if (part.boost) { 25 | return false; 26 | } 27 | parts[part.type] = true; 28 | } 29 | return parts; 30 | }; 31 | 32 | Creep.prototype.boost = function() { 33 | if (!this.room.memory.boosts) { 34 | this.memory.boosted = true; 35 | return false; 36 | } 37 | const boost = this.room.memory.boosts[this.name]; 38 | if (!boost) { 39 | this.memory.boosted = true; 40 | return false; 41 | } 42 | if (!boost.lab) { 43 | this.memory.boosted = true; 44 | delete this.room.memory.boosts[this.name]; 45 | return false; 46 | } 47 | this.log(`${JSON.stringify(boost)}`); 48 | const lab = Game.getObjectById(boost.lab); 49 | this.moveToMy(lab.pos); 50 | const response = lab.boostCreep(this); 51 | this.log(response); 52 | if (response === ERR_NOT_ENOUGH_RESOURCES) { 53 | this.memory.boosted = true; 54 | delete this.room.memory.boosts[this.name]; 55 | return false; 56 | } 57 | if (response === OK) { 58 | this.log('BOOSTED'); 59 | this.memory.boosted = true; 60 | delete this.room.memory.boosts[this.name]; 61 | return false; 62 | } 63 | return true; 64 | }; 65 | -------------------------------------------------------------------------------- /src/screepsplus.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Loads a screepspl.us grafana agent into the client. This requires that you 3 | * load the authorization token as a string into Memory.screepsplusToken. 4 | * 5 | * NOTE: This is specifically for players that do not close their clients or 6 | * do not care about data being pushed while they are not online. It will 7 | * *only* work while a client is open. 8 | * 9 | * Author: SemperRabbit (special thanks to ags131 for assisting) 10 | */ 11 | 12 | /** 13 | * runAgent - Starts the agent 14 | * 15 | * @return {void} 16 | **/ 17 | function runAgent() { 18 | Memory.screepsplusToken = config.stats.screepsPlusToken; 19 | const output = ``; 46 | console.log(output.split('\n').map((s) => s.trim()).join('')); 47 | } 48 | 49 | if (config.stats.enabled && config.stats.screepsPlusEnabled && config.stats.screepsPlusToken) { 50 | runAgent(); 51 | } 52 | -------------------------------------------------------------------------------- /src/role_powerattacker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * powerattacker kills the PowerBank 5 | * 6 | * Moves to the power bank and attack, stop attacking if its hits is below 'threshold' 7 | */ 8 | 9 | roles.powerattacker = {}; 10 | roles.powerattacker.settings = { 11 | layoutString: 'MA', 12 | amount: [5, 5], 13 | fillTough: true, 14 | }; 15 | 16 | /** 17 | * callPowerDefender 18 | * 19 | * @param {object} creep 20 | */ 21 | function callPowerDefender(creep) { 22 | if (Memory.powerBanks[creep.room.name] && !Memory.powerBanks[creep.room.name].defender) { 23 | creep.log('Call powerdefender'); 24 | Game.rooms[creep.memory.base].memory.queue.push({ 25 | role: 'powerdefender', 26 | routing: { 27 | targetRoom: creep.room.name, 28 | }, 29 | }); 30 | Memory.powerBanks[creep.room.name].defender = true; 31 | } 32 | } 33 | 34 | roles.powerattacker.action = function(creep) { 35 | const hostileCreep = creep.findClosestEnemy(); 36 | if (hostileCreep !== null) { 37 | creep.moveTo(hostileCreep); 38 | creep.attack(hostileCreep); 39 | return true; 40 | } 41 | 42 | if (creep.hits < 200) { 43 | return false; 44 | } 45 | if (hostileCreep !== null) { 46 | // I think this code is not reached 47 | callPowerDefender(creep); 48 | const range = creep.pos.getRangeTo(hostileCreep); 49 | if (range < 10) { 50 | creep.moveTo(hostileCreep); 51 | creep.attack(hostileCreep); 52 | return true; 53 | } 54 | } 55 | 56 | const powerBank = creep.room.findPropertyFilter(FIND_STRUCTURES, 'structureType', [STRUCTURE_POWER_BANK]); 57 | 58 | if (powerBank.length === 0) { 59 | if (hostileCreep !== null) { 60 | creep.moveTo(hostileCreep); 61 | creep.attack(hostileCreep); 62 | return true; 63 | } 64 | creep.move(_.random(1, 8)); 65 | return false; 66 | } 67 | 68 | if (powerBank[0].hits > 100000) { 69 | creep.spawnReplacement(); 70 | } 71 | 72 | creep.setNextSpawn(); 73 | 74 | creep.moveTo(powerBank[0]); 75 | creep.attack(powerBank[0]); 76 | return true; 77 | }; 78 | -------------------------------------------------------------------------------- /doc/Quests.md: -------------------------------------------------------------------------------- 1 | # Quests 2 | 3 | The TooAngel AI provides Quests which solvable by other players. Solving a 4 | quest brings reputation and sometimes resources, too. With increasing reputation 5 | the difficulty level of the quests increases, from build some constructionSites 6 | up to 'Room xyz needs to have an unclaimed controller in tick ...' 7 | 8 | The reputation of the players is public visible and gives an indicator how 9 | advanced other players are. And hopefully it brings even more fun 10 | playing against the TooAngel bot on a private server. 11 | 12 | And most important, the best way I could come up with to handle interaction 13 | of the TooAngel AI instances. 14 | 15 | The reputation weights other players, e.g. players with high 16 | reputation can pass through reserved or controlled rooms without punishment. 17 | Other players can send quests to the TooAngel AI, which they solved. 18 | 19 | For communication see: [API](API.md) 20 | 21 | 22 | Quests can be: 23 | - `buildcs` Build all construction sites in the given room 24 | - **tbd** Write your (or my) name with roads (or walls or creeps) in a specific room 25 | - **tbd** Bring `resource` to room 26 | - **tbd** Send `resource` via terminal to room 27 | - **tbd** Get `resource` via creep from room 28 | - **tbd** Defend specific room for some time 29 | - **tbd** Defend your room 30 | - **tbd** Attack my (or someone else) room 31 | - **tbd** Sign controller in room with `[username] smells funny` 32 | - **tbd** Dismantle structure `id` in room 33 | - **tbd** Solving math problems: Send a creep to a room and `say` math problems and the other creep need to `say` the solution 34 | - **tbd** Send a creep with random `BODYPARTS` to a room 35 | - ... 36 | 37 | If necessary the `Quester` creep will watch the progress and needs to stay alive. 38 | 39 | Next level: 40 | To introduce the bidirectional collaboration a Quest will send, to give 41 | a Quest back to our AI. After that both sides are able to send Quests to each other. 42 | 43 | To avoid misuse, requesting Quests will cost reputation. The requestable 44 | quest types depend on the reputation level. 45 | -------------------------------------------------------------------------------- /doc/Mineral.md: -------------------------------------------------------------------------------- 1 | # Mineral Handling 2 | 3 | The TooAngel bot includes comprehensive mineral management for advanced gameplay features including reactions, boosting, and market trading. 4 | 5 | ## Mining and Storage 6 | 7 | ### Basic Storage Strategy 8 | - **Room mineral**: Harvested and stored in terminal up to 500k units 9 | - **Other minerals**: Stored up to 1k units each for reaction purposes 10 | - **Base minerals**: Automatically requested via terminal transfer from other controlled rooms 11 | 12 | ### Extraction Process 13 | - Mineral extractors are built automatically when room reaches appropriate RCL 14 | - Mining operations coordinate with container placement and carry creep logistics 15 | - Extracted minerals are transported to terminal for storage and distribution 16 | 17 | ## Reactions and Processing 18 | 19 | ### Reaction Chain Management 20 | - All minerals are automatically reacted to their most advanced compounds 21 | - Reaction priority follows game's compound dependency tree 22 | - Labs are automatically filled and managed by dedicated mineral creeps 23 | 24 | ### Lab Operations 25 | - **Mineral creeps**: Specialized role responsible for filling labs with required compounds 26 | - **Reaction scheduling**: Optimizes lab usage for maximum compound production efficiency 27 | - **Boost preparation**: Ensures required compounds are available for creep boosting 28 | 29 | ## Market Integration 30 | 31 | ### Automated Trading 32 | - Minerals exceeding storage thresholds are automatically sold on the market 33 | - Market prices are monitored for optimal selling opportunities 34 | - Trade orders are managed to maximize profit while maintaining essential reserves 35 | 36 | ## Creep Boosting 37 | 38 | ### Boost Request System 39 | - Creeps request specific boosts during spawn process based on their assigned tasks 40 | - After spawning, boosted creeps automatically move to appropriate labs 41 | - Boost application is coordinated to minimize lab downtime 42 | 43 | ### Boost Types 44 | - **Military creeps**: Combat-focused boosts for attack, heal, and tough compounds 45 | - **Work creeps**: Efficiency boosts for construction, repair, and harvesting operations 46 | - **Movement creeps**: Speed enhancements for critical logistics operations -------------------------------------------------------------------------------- /src/prototype_creep_harvest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Creep.prototype.spawnCarry = function() { 4 | if (this.memory.wait > 0) { 5 | this.memory.wait -= 1; 6 | return false; 7 | } 8 | 9 | const baseRoom = Game.rooms[this.memory.base]; 10 | const creepMemory = baseRoom.creepMem('carry', this.memory.routing.targetId, this.memory.routing.targetRoom); 11 | const carrySettings = baseRoom.getSettings(creepMemory); 12 | const parts = { 13 | sourcerWork: this.body.filter((part) => part.type === WORK).length, 14 | carryParts: { 15 | move: carrySettings.amount[0], 16 | carry: carrySettings.amount[1], 17 | work: carrySettings.prefixString === '' ? 0 : 1, 18 | }, 19 | }; 20 | 21 | let resourceAtPosition = 0; 22 | const resources = this.pos.lookFor(LOOK_RESOURCES); 23 | for (const resource of resources) { 24 | resourceAtPosition += resource.amount; 25 | } 26 | 27 | const containers = this.pos.findInRange(FIND_STRUCTURES, 0, {filter: {structureType: STRUCTURE_CONTAINER}}); 28 | for (const container of containers) { 29 | resourceAtPosition += _.sum(container.store); 30 | } 31 | 32 | if (resourceAtPosition > parts.carryParts.carry * CARRY_CAPACITY || (this.memory.routing.type === 'commodity' && this.store.getFreeCapacity() === 0)) { 33 | const baseRoom = Game.rooms[this.memory.base]; 34 | if (baseRoom.hasSpawnCapacity()) { 35 | if (!baseRoom.inQueue(creepMemory)) { 36 | baseRoom.checkRoleToSpawn('carry', 0, this.memory.routing.targetId, this.memory.routing.targetRoom, carrySettings); 37 | } 38 | } 39 | } 40 | if (resourceAtPosition < 50) { 41 | const nearCarries = this.pos.findInRangeCarryWithSameTargetPower(2, this.memory.routing.targetId); 42 | if (nearCarries.length > 2) { 43 | nearCarries[0].memory.recycle = true; 44 | } 45 | } 46 | this.memory.wait = this.getCarrySpawnInterval(); 47 | return this.memory.wait; 48 | }; 49 | 50 | /** 51 | * getCarrySpawnInterval 52 | * 53 | * The interval is calculated via the time from the spawn to the source 54 | * (timeToTravel) plus some offset for delays 55 | * 56 | * @return {number} 57 | */ 58 | Creep.prototype.getCarrySpawnInterval = function() { 59 | return this.memory.timeToTravel + config.room.spawnCarryIntervalOffset; 60 | }; 61 | -------------------------------------------------------------------------------- /src/role_squadheal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * squadsiege is part of a squad to attack a room 5 | * 6 | * Heals creeps 7 | */ 8 | 9 | roles.squadheal = {}; 10 | 11 | roles.squadheal.settings = { 12 | layoutString: 'MH', 13 | amount: [1, 1], 14 | fillTough: true, 15 | }; 16 | 17 | roles.squadheal.preMove = function(creep, directions) { 18 | creep.creepLog('preMove'); 19 | if (creep.hits < creep.hitsMax) { 20 | creep.creepLog('preMove heal'); 21 | creep.selfHeal(); 22 | creep.memory.routing.reverse = true; 23 | if (directions) { 24 | directions.direction = directions.backwardDirection; 25 | } 26 | return false; 27 | } else { 28 | creep.memory.routing.reverse = false; 29 | } 30 | 31 | if (creep.healClosestCreep()) { 32 | return true; 33 | } 34 | 35 | if (creep.memory.squad) { 36 | creep.initializeSquadMembership('heal'); 37 | const squad = Memory.squads[creep.memory.squad]; 38 | if (!squad) { 39 | creep.log(`There is no squad: ${creep.memory.squad} squads: ${Object.keys(Memory.squads)}`); 40 | return false; 41 | } 42 | if (squad.action === 'move') { 43 | if (creep.squadMove(squad, 4, false, 'heal')) { 44 | return true; 45 | } 46 | } 47 | } 48 | }; 49 | 50 | roles.squadheal.action = function(creep) { 51 | creep.selfHeal(); 52 | if (creep.room.name !== creep.memory.routing.targetRoom) { 53 | // Not in target room yet - traveling 54 | if (creep.hits < creep.hitsMax) { 55 | creep.moveRandom(); 56 | } else if (!creep.pos.isBorder(-1)) { 57 | // Only reset routing if not at border to prevent bouncing 58 | delete creep.memory.routing.reached; 59 | } 60 | return true; 61 | } else { 62 | // creep.log('In room'); 63 | // TODO calculate if we would to flip directly back to the previous room 64 | // get all towers and calculate their potential damage 65 | // the damage is applied after the first tick 66 | if (creep.hits < creep.hitsMax) { 67 | creep.say('exit'); 68 | const exit = creep.pos.findClosestByRange(FIND_EXIT); 69 | creep.moveTo(exit); 70 | } else { 71 | creep.creepLog('move random'); 72 | creep.moveRandom(); 73 | creep.squadHeal(); 74 | } 75 | } 76 | 77 | return true; 78 | }; 79 | -------------------------------------------------------------------------------- /src/quests_player.js: -------------------------------------------------------------------------------- 1 | const {debugLog} = require('./logging'); 2 | 3 | /** 4 | * checkAppliedQuestForAcceptance 5 | * 6 | * @param {object} transaction 7 | * @return {bool} 8 | */ 9 | function checkAppliedQuestForAcceptance(transaction) { 10 | try { 11 | const response = JSON.parse(transaction.description); 12 | if (!response) { 13 | debugLog('quests', `No JSON.parse response: ${transaction.description}`); 14 | } 15 | if (!response.type) { 16 | debugLog('quests', `No type: ${JSON.stringify(response)}`); 17 | } 18 | if (response.type !== 'quest') { 19 | debugLog('quests', `Wrong type: ${JSON.stringify(response)}`); 20 | return false; 21 | } 22 | if (response.action) { 23 | debugLog('quests', `Action exist: ${JSON.stringify(response)}`); 24 | return false; 25 | } 26 | debugLog('quests', `Quest accept transaction: ${JSON.stringify(response)}`); 27 | if (!haveActiveQuest()) { 28 | debugLog('quests', 'No active quest'); 29 | return false; 30 | } 31 | global.data.activeQuest.state = 'active'; 32 | global.data.activeQuest.accept = response; 33 | debugLog('quests', `activeQuest: ${JSON.stringify(global.data.activeQuest)}`); 34 | if (global.data.activeQuest.accept.quest === 'buildcs') { 35 | debugLog('quests', `Should handle buildcs quest and spawn role_questplayer`); 36 | } 37 | return true; 38 | } catch (e) { 39 | console.log('checkAppliedQuestForAcceptance'); 40 | console.log(e); 41 | console.log(e.stack); 42 | return false; 43 | } 44 | } 45 | module.exports.checkAppliedQuestForAcceptance = checkAppliedQuestForAcceptance; 46 | 47 | /** 48 | * haveActiveQuest 49 | * 50 | * Check if we have an active quest as player 51 | * 52 | * @return {bool} 53 | */ 54 | function haveActiveQuest() { 55 | if (!global.data.activeQuest) { 56 | return false; 57 | } 58 | if (global.data.activeQuest.state === 'applied' && 59 | global.data.activeQuest.tick + 10 < Game.time) { 60 | debugLog('quests', 'Applied but too old, no response'); 61 | delete global.data.activeQuest; 62 | return false; 63 | } 64 | if (global.data.activeQuest.quest.end < Game.time) { 65 | delete global.data.activeQuest; 66 | return false; 67 | } 68 | return true; 69 | } 70 | 71 | module.exports.haveActiveQuest = haveActiveQuest; 72 | -------------------------------------------------------------------------------- /src/role_defendranged.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * defendranged is called after 'threshold' when a room is attacked 5 | * 6 | * Tries to fight against the hostile creeps from ramparts if possible 7 | */ 8 | 9 | roles.defendranged = {}; 10 | 11 | roles.defendranged.settings = { 12 | layoutString: 'RM', 13 | amount: [1, 1], 14 | maxLayoutAmount: 20, 15 | fillTough: true, 16 | }; 17 | 18 | // TODO This overwrites the target so redo and enable again 19 | // module.exports.action = function(creep) { 20 | // creep.memory.countdown = creep.memory.countdown || 100; 21 | // 22 | // let hostiles = creep.room.findEnemies(); 23 | // if (!hostiles.length) { 24 | // if (recycleCreep(creep)) { 25 | // return true; 26 | // } 27 | // creep.waitRampart(); 28 | // return true; 29 | // } 30 | // 31 | // hostiles = _.sortBy(hostiles, function(object) { 32 | // return creep.pos.getRangeTo(object.pos); 33 | // }); 34 | // let target = hostiles[0]; 35 | // creep.memory.countdown = 100; 36 | // creep.memory.target = target.pos; 37 | // 38 | // if (creep.fightRampart(target)) { 39 | // creep.say('fightRampart'); 40 | // return true; 41 | // } 42 | // 43 | // creep.say('fightRanged'); 44 | // return creep.fightRanged(target); 45 | // }; 46 | const action = function(creep) { 47 | creep.memory.countdown = creep.memory.countdown || 100; 48 | 49 | const recycleCreep = function(creep) { 50 | creep.say('recycle'); 51 | if (creep.room.isMy()) { 52 | if (creep.memory.countdown > 0) { 53 | creep.memory.countdown -= 1; 54 | creep.say('rnd'); 55 | creep.moveRandom(); 56 | return false; 57 | } 58 | } 59 | return Creep.recycleCreep(creep); 60 | }; 61 | 62 | let hostiles = creep.room.findEnemies(); 63 | if (!hostiles.length) { 64 | if (recycleCreep(creep)) { 65 | return true; 66 | } 67 | creep.waitRampart(); 68 | return true; 69 | } 70 | 71 | hostiles = _.sortBy(hostiles, (object) => { 72 | return creep.pos.getRangeTo(object.pos); 73 | }); 74 | const target = hostiles[0]; 75 | creep.memory.countdown = 100; 76 | creep.memory.target = target.pos; 77 | 78 | if (creep.fightRampart(target)) { 79 | creep.say('fightRampart'); 80 | return true; 81 | } 82 | 83 | creep.say('fightRanged'); 84 | return creep.fightRanged(target); 85 | }; 86 | 87 | roles.defendranged.action = action; 88 | -------------------------------------------------------------------------------- /src/role_quester.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {initPlayer, addToReputation} = require('./diplomacy'); 4 | const {debugLog} = require('./logging'); 5 | 6 | /* 7 | * quester checks if quests are solved 8 | */ 9 | 10 | roles.quester = {}; 11 | roles.quester.settings = { 12 | layoutString: 'M', 13 | maxLayoutAmount: 1, 14 | }; 15 | 16 | roles.quester.questLost = function(creep, quest, reason, value) { 17 | creep.log(`Quest lost cs: ${value} ${JSON.stringify(quest)}`); 18 | delete Memory.quests[creep.memory.level]; 19 | creep.suicide(); 20 | }; 21 | 22 | roles.quester.questWon = function(creep, quest) { 23 | const name = quest.player.name; 24 | initPlayer(name); 25 | debugLog('diplomacy', `Quest won ${JSON.stringify(quest)}`); 26 | addToReputation(name, 100); 27 | 28 | creep.log(`Quest won: ${JSON.stringify(quest)}`); 29 | const response = { 30 | type: 'quest', 31 | id: quest.id, 32 | result: 'won', 33 | }; 34 | creep.room.terminal.send(RESOURCE_ENERGY, 100, quest.player.room, JSON.stringify(response)); 35 | delete Memory.quests[creep.memory.level]; 36 | creep.suicide(); 37 | }; 38 | 39 | roles.quester.handleBuildConstructionSite = function(creep, quest) { 40 | // Give time before end to build the last CS 41 | if (quest.end - Game.time > 300) { 42 | const cs = creep.room.findConstructionSites(); 43 | if (cs.length === 0) { 44 | creep.pos.createConstructionSite(STRUCTURE_ROAD); 45 | } 46 | } 47 | if (quest.end < Game.time) { 48 | const cs = creep.room.findConstructionSites(); 49 | 50 | if (cs.length > 0) { 51 | roles.quester.questLost(creep, quest, 'cs', cs.length); 52 | return; 53 | } 54 | 55 | const roads = creep.room.findPropertyFilter(FIND_STRUCTURES, 'structureType', [STRUCTURE_ROAD]); 56 | if (roads.length < 3) { 57 | roles.quester.questLost(creep, quest, 'roads', roads.length); 58 | return; 59 | } 60 | roles.quester.questWon(creep, quest); 61 | } 62 | }; 63 | 64 | roles.quester.action = function(creep) { 65 | creep.setNextSpawn(); 66 | creep.spawnReplacement(); 67 | const quest = Memory.quests[creep.memory.level]; 68 | if (!quest) { 69 | creep.log(`Quest ${creep.memory.level} not found, suiciding`); 70 | creep.suicide(); 71 | return; 72 | } 73 | if (quest.quest === 'buildcs') { 74 | roles.quester.handleBuildConstructionSite(creep, quest); 75 | creep.moveRandom(); 76 | return true; 77 | } 78 | 79 | creep.moveRandom(); 80 | return true; 81 | }; 82 | -------------------------------------------------------------------------------- /doc/Visualization.md: -------------------------------------------------------------------------------- 1 | # Visualization Tools 2 | 3 | The TooAngel bot includes comprehensive visualization tools for debugging room layouts, creep behavior, and performance metrics. 4 | 5 | ## Room Layout Visualization 6 | 7 | ![Room Layout Visualizer](visualizer.png) 8 | 9 | ### Enable Visualization 10 | 11 | Add to your `config_local.js`: 12 | ```javascript 13 | config.visualizer.enabled = true; 14 | ``` 15 | 16 | ### Visualization Options 17 | 18 | The visualizer provides multiple overlay modes to help debug different aspects of the bot: 19 | 20 | - **`showRoomPaths`** - Displays the fixed pathfinding routes used throughout the room 21 | - **`showCreepPaths`** - Shows individual creep movement paths in red for tracking behavior 22 | - **`showPathSearches`** - Visualizes results from `PathFinder.search` operations 23 | - **`showStructures`** - Highlights planned and existing structure positions 24 | - **`showCreeps`** - Shows positions of creeps with fixed assignments 25 | - **`showBlockers`** - Displays walls, ramparts, and other movement obstacles 26 | - **`showCostMatrices`** - Shows the stored pathfinding cost matrices 27 | 28 | **Configuration:** Default values for these options are defined in `config.js` and can be overridden in `config_local.js`. 29 | 30 | ## Room Data Dashboard 31 | 32 | ![Room Data Visual](roomsDatasVisual.png) 33 | 34 | ### Default Display 35 | 36 | The room data visualization shows essential metrics for the currently selected room: 37 | 38 | - **Energy Average** - Historical energy production and consumption trends 39 | - **Energy Stored** - Current energy reserves in storage and containers 40 | - **Queue Length** - Number of creeps waiting to spawn 41 | - **RCL Progress** - Room Control Level advancement status 42 | - **Creep Queue Details** - Minimum TTL and priority information for queued creeps 43 | 44 | ### Extended Metrics 45 | 46 | Enable comprehensive data display by adding to your configuration: 47 | ```javascript 48 | config.summary.enabled = true; 49 | ``` 50 | 51 | After enabling, wait for the next summary cycle to see additional metrics including: 52 | - Resource production rates 53 | - Market activity 54 | - Combat statistics 55 | - Performance indicators 56 | - Memory usage patterns 57 | 58 | ### Usage Tips 59 | 60 | - Use visualization during development to understand bot behavior 61 | - Enable specific overlays when debugging particular issues 62 | - Room data helps optimize spawn timing and resource management 63 | - Performance overlays help identify CPU bottlenecks 64 | -------------------------------------------------------------------------------- /src/role_powerhealer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * powerhealer is used to heal powerattacker 5 | * 6 | * Moves to the room where the PowerBank is and heals surrounding creeps 7 | */ 8 | 9 | roles.powerhealer = {}; 10 | 11 | roles.powerhealer.settings = { 12 | layoutString: 'MH', 13 | maxLayoutAmount: 21, 14 | }; 15 | 16 | roles.powerhealer.action = function(creep) { 17 | creep.selfHeal(); 18 | const myCreep = creep.pos.findClosestByRange(FIND_MY_CREEPS, { 19 | filter: (object) => object.hits < object.hitsMax, 20 | }); 21 | 22 | if (myCreep !== null) { 23 | const range = creep.pos.getRangeTo(myCreep); 24 | if (range > 1) { 25 | creep.rangedHeal(myCreep); 26 | } else { 27 | creep.heal(myCreep); 28 | } 29 | } 30 | 31 | roles.powerhealer.heal(creep); 32 | return true; 33 | }; 34 | 35 | /** 36 | * healCreep 37 | * 38 | * @param {object} creep 39 | * @param {object} creepToHeal 40 | */ 41 | function healCreep(creep, creepToHeal) { 42 | const range = creep.pos.getRangeTo(creepToHeal); 43 | if (range > 1) { 44 | creep.rangedHeal(creepToHeal); 45 | } else { 46 | creep.heal(creepToHeal); 47 | } 48 | creep.moveTo(creepToHeal); 49 | } 50 | 51 | roles.powerhealer.heal = function(creep) { 52 | let creepToHeal = creep.pos.findClosestByRange(FIND_MY_CREEPS, { 53 | filter: (object) => object.hits < object.hitsMax / 1.5, 54 | }); 55 | const powerBank = creep.room.findPropertyFilter(FIND_STRUCTURES, 'structureType', [STRUCTURE_POWER_BANK]); 56 | if (powerBank.length > 0 && powerBank[0].hits > 100000) { 57 | creep.setNextSpawn(); 58 | creep.spawnReplacement(); 59 | } 60 | 61 | let attacker; 62 | 63 | if (creepToHeal) { 64 | healCreep(this, creepToHeal); 65 | return true; 66 | } 67 | 68 | if (powerBank.length === 0) { 69 | creepToHeal = creep.pos.findClosestByRange(FIND_MY_CREEPS, { 70 | filter: (object) => object.hits < object.hitsMax, 71 | }); 72 | if (creepToHeal) { 73 | healCreep(this, creepToHeal); 74 | return true; 75 | } 76 | attacker = creep.pos.findClosestByRangeCreepPowerAttacker(); 77 | creep.moveTo(attacker); 78 | return false; 79 | } 80 | const hostileCreeps = creep.room.findEnemies(); 81 | if (hostileCreeps.length > 0) { 82 | attacker = creep.pos.findClosestByRangeCreepPowerAttacker(); 83 | creep.moveTo(attacker); 84 | return false; 85 | } 86 | const range = creep.pos.getRangeTo(powerBank[0]); 87 | if (range > 2) { 88 | creep.moveTo(powerBank[0]); 89 | } 90 | return false; 91 | }; 92 | -------------------------------------------------------------------------------- /utils/testConfig.js: -------------------------------------------------------------------------------- 1 | module.exports.cliPort = 21026; 2 | 3 | module.exports.verbose = false; 4 | 5 | module.exports.tickDuration = 10; 6 | 7 | // todo for local-testing 8 | // if your machine is slow try increment this 9 | module.exports.waitForConnection = 10; 10 | 11 | module.exports.playerRoom = 'W8N8'; 12 | const players = { 13 | 'W1N7': {x: 43, y: 35}, 14 | 'W8N8': {x: 21, y: 28}, 15 | 'W8N1': {x: 33, y: 13}, 16 | 'W5N1': {x: 10, y: 9}, 17 | 'W8N3': {x: 14, y: 17}, 18 | 'W7N4': {x: 36, y: 11}, 19 | 'W2N5': {x: 8, y: 26}, 20 | }; 21 | module.exports.players = players; 22 | module.exports.rooms = Object.keys(players); 23 | 24 | module.exports.milestones = [ 25 | {tick: 500, check: {structures: 1}, required: true}, 26 | {tick: 1000, check: {level: 2}, required: true}, 27 | {tick: 2000, check: {structures: 2}, required: true}, 28 | {tick: 3300, check: {structures: 3}, required: true}, 29 | {tick: 3700, check: {structures: 4}, required: true}, 30 | {tick: 4300, check: {structures: 5}, required: true}, 31 | {tick: 4900, check: {structures: 6}, required: true}, 32 | {tick: 14100, check: {level: 3}, required: true}, 33 | {tick: 14200, check: {structures: 7}, required: true}, 34 | {tick: 14300, check: {structures: 8}, required: true}, 35 | {tick: 14800, check: {structures: 9}, required: true}, 36 | {tick: 15300, check: {structures: 10}, required: true}, 37 | {tick: 15700, check: {structures: 11}, required: true}, 38 | {tick: 30000, check: {level: 4}, required: false}, 39 | {tick: 30100, check: {structures: 12}, required: false}, 40 | {tick: 30400, check: {structures: 13}, required: false}, 41 | {tick: 30500, check: {structures: 14}, required: false}, 42 | {tick: 30800, check: {structures: 15}, required: false}, 43 | {tick: 31000, check: {structures: 16}, required: false}, 44 | {tick: 31400, check: {structures: 17}, required: false}, 45 | {tick: 31600, check: {structures: 18}, required: false}, 46 | {tick: 32000, check: {structures: 19}, required: false}, 47 | {tick: 32500, check: {structures: 20}, required: false}, 48 | {tick: 33000, check: {structures: 21}, required: false}, 49 | {tick: 37000, check: {structures: 22}, required: false}, 50 | {tick: 49000, check: {structures: 23}, required: false}, 51 | {tick: 50000, check: {structures: 24}}, 52 | {tick: 51000, check: {structures: 25}}, 53 | {tick: 52000, check: {structures: 26}}, 54 | {tick: 53000, check: {structures: 27}}, 55 | {tick: 54000, check: {structures: 28}}, 56 | {tick: 55000, check: {structures: 29}}, 57 | {tick: 56000, check: {structures: 30}}, 58 | ]; 59 | -------------------------------------------------------------------------------- /src/require.js: -------------------------------------------------------------------------------- 1 | require('./config'); 2 | require('./logging'); 3 | require('./utils'); 4 | require('./brain_main'); 5 | require('./brain_memory'); 6 | require('./brain_memory_market'); 7 | require('./brain_nextroom'); 8 | require('./brain_squadmanager'); 9 | require('./brain_stats'); 10 | require('./find'); 11 | require('./prototype_creep'); 12 | require('./prototype_creep_clean'); 13 | require('./prototype_creep_heal'); 14 | require('./prototype_creep_fight'); 15 | require('./prototype_creep_resources'); 16 | require('./prototype_creep_harvest'); 17 | require('./prototype_creep_mineral'); 18 | require('./prototype_creep_routing'); 19 | require('./prototype_roomPosition_structures'); 20 | require('./prototype_room'); 21 | require('./prototype_room_basebuilder'); 22 | require('./prototype_room_controller'); 23 | require('./prototype_room_creepbuilder'); 24 | require('./prototype_room_defense'); 25 | require('./prototype_room_find'); 26 | require('./prototype_room_keeper'); 27 | require('./prototype_room_market'); 28 | require('./prototype_room_memory'); 29 | require('./prototype_room_mineral'); 30 | require('./prototype_room_my'); 31 | require('./prototype_room_external'); 32 | require('./prototype_room_routing'); 33 | require('./prototype_room_utils'); 34 | require('./prototype_room_wallsetter'); 35 | require('./prototype_string'); 36 | require('./prototype_structure'); 37 | require('./prototype_structureController'); 38 | require('./prototype_structureStorage'); 39 | require('./role_atkeeper'); 40 | require('./role_atkeepermelee'); 41 | require('./role_attackunreserve'); 42 | require('./role_autoattackmelee'); 43 | require('./role_builder'); 44 | require('./role_carry'); 45 | require('./role_claimer'); 46 | require('./role_defender'); 47 | require('./role_defendmelee'); 48 | require('./role_defendranged'); 49 | require('./role_extractor'); 50 | require('./role_mineral'); 51 | require('./role_nextroomer'); 52 | require('./role_nextroomerattack'); 53 | require('./role_powerattacker'); 54 | require('./role_powerdefender'); 55 | require('./role_powerhealer'); 56 | require('./role_powertransporter'); 57 | require('./role_quester'); 58 | require('./role_questplayer'); 59 | require('./role_reserver'); 60 | require('./role_repairer'); 61 | require('./role_scout'); 62 | require('./role_signer'); 63 | require('./role_sourcer'); 64 | require('./role_squadsiege'); 65 | require('./role_squadheal'); 66 | require('./role_storagefiller'); 67 | require('./role_structurer'); 68 | require('./role_towerdrainer'); 69 | require('./role_universal'); 70 | require('./role_upgrader'); 71 | require('./role_watcher'); 72 | require('./prototype_creep_squad'); 73 | -------------------------------------------------------------------------------- /src/role_towerdrainer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * TowerDrainer is used to drain energy from hostile towers 5 | * 6 | * Moves to the border between routing targetRoom and attackRoom, 7 | * step for one tick into attackRoom, then step out and heals 8 | * 9 | * You need 13 towerDrainer to drain at maximum speed 10 | * 11 | * Towerdrainer doesn't called now from anywhere. Call them manually: 12 | * @example 13 | * Game.rooms.E17N1.memory.queue.push({role: 'towerdrainer', routing: {targetRoom: 'E16N0'}, attackRoom: 'E16N1'}) 14 | */ 15 | 16 | roles.towerdrainer = {}; 17 | 18 | roles.towerdrainer.settings = { 19 | layoutString: 'TMH', 20 | amount: [3, 5, 2], // attack RCL 5 21 | // amount: [2, 3, 1], // attack RCL 3 22 | maxLayoutAmount: 1, 23 | }; 24 | 25 | roles.towerdrainer.getRestPosition = function(creep) { 26 | const restRoom = creep.memory.routing.targetRoom; 27 | const attackRoom = creep.memory.attackRoom; 28 | if (!creep.memory.restPosition) { 29 | creep.notifyWhenAttacked(false); 30 | const room = Game.rooms[restRoom]; 31 | const attackDirection = room.findExitTo(attackRoom); 32 | const restDirection = RoomPosition.oppositeDirection(attackDirection); 33 | const occupiedPositions = {}; 34 | _.filter(Game.creeps, (c) => c.memory.role === 'towerdrainer' && c.memory.restPosition).forEach((c) => { 35 | occupiedPositions[c.memory.restPosition.x + c.memory.restPosition.y] = c.id; 36 | }); 37 | const attackExits = room.find(attackDirection); 38 | for (const exit of attackExits) { 39 | const pos = exit.getAdjacentPosition(restDirection); 40 | if (!pos.checkForWall() && !pos.checkForObstacleStructure() && !occupiedPositions[pos.x + pos.y]) { 41 | creep.memory.restPosition = pos; 42 | creep.memory.attackDirrection = attackDirection; 43 | creep.memory.restDirrection = restDirection; 44 | break; 45 | } 46 | } 47 | } 48 | return creep.memory.restPosition; 49 | }; 50 | 51 | roles.towerdrainer.action = function(creep) { 52 | const attackRoom = creep.memory.attackRoom; 53 | const restPos = roles.towerdrainer.getRestPosition(creep); 54 | if (!restPos) { 55 | creep.log('no position'); 56 | creep.moveRandom(); 57 | return false; 58 | } 59 | creep.selfHeal(); 60 | 61 | if (creep.pos.roomName === attackRoom || creep.pos.isBorder(-1) && creep.pos.isNearTo(restPos.x, restPos.y)) { 62 | creep.move(creep.memory.restDirrection); 63 | } else if (creep.pos.isEqualTo(restPos.x, restPos.y)) { 64 | creep.move(creep.memory.attackDirrection); 65 | } else { 66 | creep.moveTo(restPos.x, restPos.y); 67 | } 68 | 69 | return true; 70 | }; 71 | -------------------------------------------------------------------------------- /doc/CodeBase.md: -------------------------------------------------------------------------------- 1 | # Code base 2 | 3 | ## Developing on test server 4 | 5 | To setup a test server with multiple bots. This will prepare the server and start everyting in a docker compose environment and can be stopped with CTRL-C 6 | 7 | - `npm run setupTestServer` 8 | 9 | It can be resumed via 10 | 11 | `docker compose up` 12 | 13 | To deploy create a `.screeps.yaml`: 14 | ``` 15 | servers: 16 | main: 17 | host: 127.0.0.1 18 | port: 21025 19 | secure: false 20 | username: 'W8N8' 21 | password: 'tooangel' 22 | ``` 23 | 24 | and run `npm run deployLocal`. 25 | 26 | 27 | ## Tweaking 28 | 29 | ### Config 30 | Add a `src/config_local.js` to overwrite configuration values. Copy 31 | `config_local.js.example` to `src/config_local.js` as an example. `src/config.js` 32 | has the default values. 33 | 34 | ### Friends 35 | Add a `src/friends.js` with player names to ignore them from all attack considerations. 36 | 37 | E.g.: 38 | `module.exports = ['TooAngel'];` 39 | 40 | 41 | ## Interacting 42 | 43 | the `src/utils.js` provides methods to interact with the NPC. 44 | 45 | ## Debugging 46 | 47 | Within the `config_local.js` certain `config.debug` flags can be enabled. 48 | To add debug messages `Room.debugLog(TYPE, MESSAGE)` and 49 | `Creep.creepLog(MESSAGE)` are suggested. Especially the `creepLog` allows 50 | granular output of the creep behavior based on the room and the creep role. 51 | 52 | ## Testing 53 | 54 | `node utils/test.js` will start a private server and add some bots as test cases. 55 | 56 | ## Upload 57 | 58 | install dependencies 59 | 60 | npm install 61 | 62 | add your account credentials 63 | 64 | ### to screeps.com 65 | 66 | To deploy to the live server provide the credentials. 67 | 68 | #### via env 69 | 70 | export email=EMAIL 71 | export password=PASSWORD 72 | 73 | #### via git ignored file 74 | 75 | echo "module.exports = { email: 'your-email@here.tld', password: 'your-secret' };" > account.screeps.com.js 76 | or edit and rename account.screeps.com.js.sample to account.screeps.com.js 77 | 78 | And deploy to the server: 79 | 80 | grunt 81 | 82 | ### to private server 83 | Create a `.localSync.js` file with content: 84 | ``` 85 | module.exports = [{ 86 | cwd: 'src', 87 | src: [ 88 | '*.js' 89 | ], 90 | dest: '$HOME/.config/Screeps/scripts/SERVER/default', 91 | }]; 92 | ``` 93 | 94 | grunt local 95 | 96 | ## Release 97 | 98 | Releasing to npm is done automatically by increasing the version and merging to `master`. 99 | 100 | Every deploy to `master` is automatically deployed to the live tooangel account. 101 | -------------------------------------------------------------------------------- /src/role_atkeeper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * atkeeper is used to kill Source Keeper (ranged version) 5 | * 6 | * Attacks source keeper, move away when hits below 'threshold' 7 | * If no source keeper is available move to position where the next will spawn 8 | */ 9 | 10 | roles.atkeeper = {}; 11 | 12 | roles.atkeeper.settings = { 13 | layoutString: 'MRH', 14 | amount: [18, 6, 12], 15 | fillTough: false, 16 | }; 17 | 18 | roles.atkeeper.preMove = function(creep) { 19 | creep.checkForRoutingReached(); 20 | }; 21 | 22 | roles.atkeeper.action = function(creep) { 23 | // TODO Untested 24 | creep.spawnReplacement(); 25 | creep.setNextSpawn(); 26 | const center = new RoomPosition(25, 25, creep.memory.routing.targetRoom); 27 | const moveToCenter = function(creep, near) { 28 | near = near || 10; 29 | if (Game.time % 3 > 0) { 30 | return creep.moveToMy(center, near); 31 | } else { 32 | return creep.moveRandomWithin(center, near); 33 | } 34 | }; 35 | const healAndMove = function(creep) { 36 | let creepsDamaged = creep.room.findDamagedCreeps(); 37 | let creepsNearDamaged = creep.pos.findInRangeDamagedCreeps(3); 38 | 39 | creepsDamaged = _.sortBy(creepsDamaged, (c)=> c.isDamaged()); 40 | creepsNearDamaged = _.sortBy(creepsNearDamaged, (c)=> c.isDamaged()); 41 | if (creepsDamaged.length > 0 || creepsNearDamaged.length > 0) { 42 | const first = creepsNearDamaged[0] || creepsDamaged[0]; 43 | if (first && first.pos) { 44 | creep.heal(first); 45 | creep.rangedHeal(first); 46 | if (center.getRangeTo(first) < 12) { 47 | creep.moveTo(first.pos, {ignoreCreeps: false, reusePath: 2}); 48 | } else if ((first.memory.role === 'sourcer') || (first.memory.role === 'extractor')) { 49 | creep.moveTo(first.pos, {ignoreCreeps: false, reusePath: 2}); 50 | } 51 | return true; 52 | } 53 | } 54 | return false; 55 | }; 56 | const fightRangedInvaders = function(creep) { 57 | if (creep.getActiveBodyparts(RANGED_ATTACK) === 0) { 58 | return false; 59 | } 60 | const hostile = creep.pos.findClosestByRangeSystemCreeps(); 61 | if (hostile) { 62 | return creep.fightRanged(hostile); 63 | } 64 | return false; 65 | }; 66 | 67 | if (creep.room.name === creep.memory.routing.targetRoom) { 68 | if (!fightRangedInvaders(creep)) { 69 | moveToCenter(creep); 70 | if (healAndMove(creep)) { 71 | return true; 72 | } 73 | if (creep.room.keeperTeamReady()) { 74 | if (healAndMove(creep)) { 75 | return true; 76 | } 77 | } 78 | } else { 79 | creep.selfHeal(); 80 | return true; 81 | } 82 | } 83 | 84 | return moveToCenter(creep); 85 | }; 86 | -------------------------------------------------------------------------------- /src/role_storagefiller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * storagefiller should be present on RCL > 4 5 | * 6 | * Gets the energy from the link and transfers it to the tower or storage 7 | * 8 | */ 9 | 10 | roles.storagefiller = {}; 11 | roles.storagefiller.killPrevious = true; 12 | 13 | roles.storagefiller.settings = { 14 | layoutString: 'MC', 15 | amount: [1, 4], 16 | maxLayoutAmount: 1, 17 | }; 18 | 19 | /** 20 | * transferFromLink 21 | * @param {object} creep 22 | * @return {boolean} 23 | */ 24 | function transferFromLink(creep) { 25 | const tower = getTower(creep); 26 | const link = creep.getCloseByLink(); 27 | if (!link) { 28 | creep.withdraw(creep.room.storage, RESOURCE_ENERGY); 29 | creep.transfer(tower, RESOURCE_ENERGY); 30 | return true; 31 | } 32 | 33 | creep.withdraw(link, RESOURCE_ENERGY); 34 | if (tower && tower.store.getFreeCapacity(RESOURCE_ENERGY) > creep.store[RESOURCE_ENERGY]) { 35 | creep.transfer(tower, RESOURCE_ENERGY); 36 | return true; 37 | } 38 | 39 | const result = creep.transfer(creep.room.storage, RESOURCE_ENERGY); 40 | return result === OK; 41 | } 42 | 43 | /** 44 | * getTower - Gets the tower from heap data, or sets if missing 45 | * 46 | * @param {object} creep - The creep 47 | * @return {object} - The tower 48 | **/ 49 | function getTower(creep) { 50 | if (!creep.data.tower) { 51 | const structures = creep.pos.findInRange(FIND_MY_STRUCTURES, 1, {filter: {structureType: STRUCTURE_TOWER}}); 52 | if (structures.length === 0) { 53 | return; 54 | } 55 | creep.data.tower = structures[0].id; 56 | } 57 | return Game.getObjectById(creep.data.tower); 58 | } 59 | 60 | /** 61 | * getPowerSpawn - Gets the powerSpawn from heap data, or sets if missing 62 | * 63 | * @param {object} creep - The creep 64 | * @return {object} - The powerSpawn 65 | **/ 66 | function getPowerSpawn(creep) { 67 | if (!creep.data.powerSpawn) { 68 | const structures = creep.pos.findInRange(FIND_MY_STRUCTURES, 1, {filter: {structureType: STRUCTURE_POWER_SPAWN}}); 69 | if (structures.length === 0) { 70 | return; 71 | } 72 | creep.data.powerSpawn = structures[0].id; 73 | } 74 | return Game.getObjectById(creep.data.powerSpawn); 75 | } 76 | 77 | roles.storagefiller.action = function(creep) { 78 | creep.setNextSpawn(); 79 | creep.spawnReplacement(1); 80 | 81 | if (transferFromLink(creep)) { 82 | return true; 83 | } 84 | 85 | if (creep.room.controller.level === 8) { 86 | const powerSpawn = getPowerSpawn(creep); 87 | if (powerSpawn) { 88 | if (creep.room.storage.store[RESOURCE_POWER] > 0) { 89 | creep.withdraw(creep.room.storage, RESOURCE_POWER); 90 | creep.transfer(powerSpawn, RESOURCE_POWER); 91 | } 92 | } 93 | } 94 | return true; 95 | }; 96 | -------------------------------------------------------------------------------- /doc/API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | The API is all about reputation and collaboration via Quests. 4 | Reputation equals somehow a credit value, actions are mostly based on the current market value of a certain item. E.g. attacking the TooAngel NPC with a Nuke reduces the reputation with a market value of `CPU_UNLOCK`. 5 | 6 | ## Increase reputation via resources 7 | 8 | - Sending resources via terminal transfer increases the reputation based on the market value of the resource. 9 | 10 | ## Information about the reputation 11 | 12 | ### Highscores 13 | 14 | The TooAngel NPC provides two public memory segments 1 and 2. 15 | - Segment 1 shows the top 10 players (highest reputation) 16 | - Segment 2 shows the bottom 10 players (lowest reputation) 17 | 18 | ### Request reputation 19 | 20 | To get your current reputation send a terminal transfer to one of the TooAngel NPC rooms. 21 | Sufficient energy needs to be send, so that the TooAngel NPC can send a response. 22 | 23 | `{"type": "reputation"}` 24 | 25 | Response: 26 | 27 | `{"type": "reputation", "reputation": "REPUTATION"}` 28 | 29 | ## Quests API 30 | 31 | ### Apply for quests (optional, mainly used to increase visibility of the quest feature) 32 | 33 | The TooAngel NPC provides quests via controller signs. This allows player to apply for quests and gives visibility of the Quests feature for other players. 34 | The sign has the format: 35 | 36 | ``` 37 | { 38 | "type": "quest", 39 | "id": "QUEST_ID", 40 | "origin": "ROOM_NAME", 41 | "info": "http://tooangel.github.io/screeps" 42 | } 43 | ``` 44 | 45 | To apply for a Quest send a message via terminal transfer to the `origin` room, with the content: 46 | 47 | ``` 48 | { 49 | "type": "quest", 50 | "id": "QUEST_ID", 51 | "action": "apply" 52 | } 53 | ``` 54 | 55 | The actual quest will be send to the room the transfer was initiated from. 56 | 57 | ## Getting / Sending quests 58 | 59 | Quests can be received from the TooAngel NPC and also send to the TooAngel NPC. 60 | When quests are solved the reputation increases. 61 | When quests are send to the TooAngel NPC the reputation is decreased. 62 | 63 | ### Quest format 64 | 65 | Quests are send via terminal transfer: 66 | 67 | ``` 68 | { 69 | "type": "quest", 70 | "id": "QUEST_ID", 71 | "room": "ROOM_NAME, in which the quest needs to be solved", 72 | "quest": "TYPE OF QUEST", 73 | "end": "Game.time when the quest needs to be finished" 74 | } 75 | ``` 76 | 77 | Quest types can be found in [Quests](Quests.md) 78 | 79 | ### Quest completed (optional) 80 | 81 | The TooAngel NPC sends a quest completion message, with a certain amount of resources. This is not necessary when sending quests to the TooAngel NPC. 82 | Internally the reputation is increased. 83 | 84 | ``` 85 | { 86 | "type": "quest", 87 | "id": "QUEST_ID", 88 | "result": "won" 89 | }; 90 | ``` -------------------------------------------------------------------------------- /src/role_squadsiege.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * squadsiege is part of a squad to attack a room 5 | * 6 | * Attacks structures, runs away if I will be destroyed (hopefully) 7 | */ 8 | 9 | roles.squadsiege = {}; 10 | 11 | roles.squadsiege.settings = { 12 | layoutString: 'MW', 13 | maxLayoutAmount: 21, 14 | fillTough: true, 15 | }; 16 | 17 | roles.squadsiege.dismantleSurroundingStructures = function(creep, directions) { 18 | if (!directions || !directions.forwardDirection) { 19 | return false; 20 | } 21 | const posForward = creep.pos.getAdjacentPosition(directions.forwardDirection); 22 | const structures = posForward.lookFor(LOOK_STRUCTURES); 23 | const ramparts = []; 24 | const walls = []; 25 | for (const structure of structures) { 26 | if (structure.my) { 27 | continue; 28 | } 29 | switch (structure.structureType) { 30 | case STRUCTURE_ROAD: 31 | case STRUCTURE_CONTROLLER: 32 | case STRUCTURE_KEEPER_LAIR: 33 | continue; 34 | case STRUCTURE_RAMPART: 35 | ramparts.push(structure); 36 | continue; 37 | case STRUCTURE_WALL: 38 | walls.push(structure); 39 | continue; 40 | default: 41 | // do nothing 42 | } 43 | creep.dismantle(structure); 44 | creep.say('dismantle : ' + structure.id); 45 | return true; 46 | } 47 | // if no any other better structures to dismantle, we can only go for rampart and wall... 48 | if (ramparts.length) { 49 | creep.dismantle(ramparts[0]); 50 | creep.say('dismantle : ' + ramparts[0].id); 51 | return true; 52 | } 53 | if (walls.length) { 54 | creep.dismantle(walls[0]); 55 | creep.say('dismantle : ' + walls[0].id); 56 | return true; 57 | } 58 | return false; 59 | }; 60 | 61 | roles.squadsiege.preMove = function(creep, directions) { 62 | // creep.log('preMove'); 63 | if (!directions) { 64 | return false; 65 | } 66 | roles.squadsiege.dismantleSurroundingStructures(creep, directions); 67 | if (creep.memory.squad) { 68 | creep.initializeSquadMembership('siege'); 69 | const squad = Memory.squads[creep.memory.squad]; 70 | if (squad.action === 'move') { 71 | if (creep.squadMove(squad, 2, true, 'siege')) { 72 | return true; 73 | } 74 | } 75 | } 76 | return false; 77 | }; 78 | 79 | roles.squadsiege.action = function(creep) { 80 | creep.say('action'); 81 | 82 | if (creep.room.name !== creep.memory.routing.targetRoom) { 83 | // Not in target room yet - traveling 84 | if (creep.hits < creep.hitsMax) { 85 | creep.moveRandom(); 86 | } else if (!creep.pos.isBorder(-1)) { 87 | // Only reset routing if not at border to prevent bouncing 88 | delete creep.memory.routing.reached; 89 | } 90 | return true; // Don't execute siege when traveling 91 | } 92 | 93 | // In target room - execute siege action 94 | return creep.siege(); 95 | }; 96 | -------------------------------------------------------------------------------- /src/helper_findMyRooms.js: -------------------------------------------------------------------------------- 1 | /** 2 | * findRoomsWithinReach - finds rooms within reach for nextRooms 3 | * 4 | * @param {object} room - The room to search for 5 | * @return {string[]} - A list of room names 6 | **/ 7 | function findRoomsWithinReach(room) { 8 | const rooms = []; 9 | for (const myRoom of Memory.myRooms) { 10 | try { 11 | const distance = Game.map.getRoomLinearDistance(room, myRoom); 12 | // console.log(`roomWithinReach room: ${room} myRoom: ${myRoom} distance: ${distance}`); 13 | if (distance < config.nextRoom.maxDistance) { 14 | rooms.push(myRoom); 15 | } 16 | } catch (e) { 17 | console.log(`Exception: helper_findMyRooms.findRoomsWithinReach ${e} ${room} ${myRoom}`); 18 | continue; 19 | } 20 | } 21 | return rooms; 22 | } 23 | module.exports.findRoomsWithinReach = findRoomsWithinReach; 24 | 25 | /** 26 | * findMyRoomsSortByDistance - Returns myRooms sorted by distance 27 | * 28 | * @param {string} roomName - The roomName to calculate the distance to 29 | * @return {string[]} - MyRoom names sorted by distance 30 | **/ 31 | function findMyRoomsSortByDistance(roomName) { 32 | const sortByDistance = (object) => { 33 | return Game.map.getRoomLinearDistance(roomName, object); 34 | }; 35 | 36 | return _.sortBy(Memory.myRooms, sortByDistance); 37 | } 38 | module.exports.findMyRoomsSortByDistance = findMyRoomsSortByDistance; 39 | 40 | /** 41 | * getMyRoomWithinRange 42 | * 43 | * @param {string} roomName - The room the distance to check 44 | * @param {number} range - The max range 45 | * @param {number} minRCL - The min RCL a room needs to have 46 | * @param {number} minStorageEnergyPercentage - The min RCL a room needs to have 47 | * @return {string|boolean} - A room name in range or `false` otherwise 48 | **/ 49 | function getMyRoomWithinRange(roomName, range=0, minRCL=0, minStorageEnergyPercentage=0) { 50 | // TODO Instead of just finding one room, it should be the closest (or highest RCL) 51 | for (const myRoomName of Memory.myRooms) { 52 | const room = Game.rooms[myRoomName]; 53 | if (!room) { 54 | console.log(`helper_findMyRooms.getMyRoomWithinRange room not available: ${myRoomName} ${room}`); 55 | continue; 56 | } 57 | if (minRCL && room.controller.level < minRCL) { 58 | continue; 59 | } 60 | if (!room.hasSpawnCapacity()) { 61 | continue; 62 | } 63 | if (!room.isHealthy()) { 64 | continue; 65 | } 66 | if (room.storage.store.getUsedCapacity(RESOURCE_ENERGY) < minStorageEnergyPercentage * room.storage.store.getCapacity(RESOURCE_ENERGY)) { 67 | continue; 68 | } 69 | const distance = Game.map.getRoomLinearDistance(roomName, myRoomName); 70 | if (range && distance > range) { 71 | continue; 72 | } 73 | return myRoomName; 74 | } 75 | return false; 76 | } 77 | module.exports.getMyRoomWithinRange = getMyRoomWithinRange; 78 | -------------------------------------------------------------------------------- /utils/grafana.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const rp = require('request-promise-native'); 4 | const fs = require('fs'); 5 | 6 | const token = process.env.grafana_tooangel_token; 7 | 8 | /** 9 | * getDashboard 10 | * 11 | * @param {string} name 12 | * @return {object} 13 | */ 14 | async function getDashboard(name) { 15 | const url = 'https://screepspl.us/grafana/api/dashboards/' + name; 16 | const dashboard = await rp.get({ 17 | uri: url, 18 | auth: { 19 | 'bearer': token, 20 | }, 21 | json: true, 22 | }); 23 | return dashboard; 24 | } 25 | 26 | /** 27 | * getDashboardList 28 | * @return {list} 29 | */ 30 | async function getDashboardList() { 31 | const url = 'https://screepspl.us/grafana/api/search'; 32 | const list = await rp.get({ 33 | uri: url, 34 | auth: { 35 | 'bearer': token, 36 | }, 37 | json: true, 38 | }); 39 | return list; 40 | } 41 | 42 | /** 43 | * getDashboards 44 | */ 45 | async function getDashboards() { 46 | const list = await getDashboardList(); 47 | 48 | for (const item of Object.keys(list)) { 49 | const path = list[item].uri; 50 | console.log(path); 51 | const obj = JSON.parse(fs.readFileSync('grafana/main/' + path + '.json', 'utf8')); 52 | const dashboard = await getDashboard(path); 53 | if (obj.dashboard.version === dashboard.dashboard.version) { 54 | continue; 55 | } 56 | if (obj.dashboard.version < dashboard.dashboard.version) { 57 | console.log(`Dashboard: ${JSON.stringify(dashboard)}`); 58 | console.log(`File: ${JSON.stringify(obj)}`); 59 | throw Error(`Dashboard ${path} was changed online`); 60 | } 61 | console.log(`Updating ${path}`); 62 | updateDashboard(path); 63 | break; 64 | } 65 | } 66 | 67 | /** 68 | * updateDashboard 69 | * 70 | * @param {string} path 71 | */ 72 | async function updateDashboard(path) { 73 | const obj = JSON.parse(fs.readFileSync('grafana/main/' + path + '.json', 'utf8')); 74 | 75 | let url = 'https://screepspl.us/grafana/api/search'; 76 | url = 'https://screepspl.us/grafana/api/dashboards/db'; 77 | console.log(url); 78 | 79 | const options = { 80 | uri: url, 81 | method: 'POST', 82 | json: obj, 83 | auth: { 84 | bearer: token, 85 | }, 86 | }; 87 | 88 | const response = await rp(options); 89 | console.log(response); 90 | } 91 | 92 | // async function fetchDashboards() { 93 | // const list = await getDashboardList(); 94 | // for (const item of Object.keys(list)) { 95 | // const dashboard = await getDashboard(list[item].uri); 96 | // fs.writeFileSync('grafana/tmp/' + list[item].uri + '.json', JSON.stringify(dashboard), 'utf8'); 97 | // } 98 | // } 99 | 100 | /** 101 | * main 102 | */ 103 | function main() { 104 | getDashboards(); 105 | 106 | // To download dashboard uncomment 107 | // fetchDashboards(); 108 | } 109 | 110 | main(); 111 | -------------------------------------------------------------------------------- /src/role_autoattackmelee.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {cleanUpDyingCreep} = require('./brain_main'); 4 | 5 | /* 6 | * autoattackmelee is the first wave of auto attacks 7 | * 8 | * Kills tower and spawn, hostile creeps and construction sites 9 | */ 10 | 11 | roles.autoattackmelee = {}; 12 | 13 | roles.autoattackmelee.settings = { 14 | layoutString: 'MA', 15 | amount: [5, 5], 16 | fillTough: true, 17 | }; 18 | 19 | roles.autoattackmelee.died = function(name) { 20 | cleanUpDyingCreep(name); 21 | }; 22 | 23 | roles.autoattackmelee.preMove = function(creep) { 24 | creep.creepLog('!!!!!!!!!!!!!!!! Auto Attacking'); 25 | }; 26 | 27 | /** 28 | * Sends a attack notification via mail when the attack creep is spawned 29 | * 30 | * @param {object} creep 31 | */ 32 | function sendAttackNotification(creep) { 33 | if (config.autoAttack.notify && !creep.memory.notified) { 34 | creep.log('Attacking'); 35 | Game.notify(Game.time + ' ' + creep.room.name + ' Attacking'); 36 | creep.memory.notified = true; 37 | } 38 | } 39 | 40 | /** 41 | * attackWithoutSpawn 42 | * 43 | * @param {object} creep 44 | */ 45 | function attackWithoutSpawn(creep) { 46 | const hostileCreep = creep.findClosestEnemy(); 47 | if (hostileCreep) { 48 | creep.moveTo(hostileCreep); 49 | creep.attack(hostileCreep); 50 | return; 51 | } 52 | 53 | const structures = creep.pos.findClosestByRangeHostileStructures(); 54 | if (!structures) { 55 | const constructionSites = creep.pos.findClosestByRange(FIND_CONSTRUCTION_SITES); 56 | creep.moveTo(constructionSites); 57 | return; 58 | } 59 | creep.moveTo(structures); 60 | creep.attack(structures); 61 | } 62 | 63 | roles.autoattackmelee.action = function(creep) { 64 | sendAttackNotification(creep); 65 | 66 | if (creep.room.name !== creep.memory.routing.targetRoom) { 67 | creep.memory.routing.reached = false; 68 | return true; 69 | } 70 | 71 | if (creep.room.controller && creep.room.controller.safeMode) { 72 | const constructionSites = creep.room.findConstructionSites(); 73 | creep.moveTo(constructionSites[0]); 74 | return true; 75 | } 76 | 77 | const spawn = creep.pos.findClosestByRangeHostileSpawn(); 78 | if (!spawn) { 79 | attackWithoutSpawn(creep); 80 | return true; 81 | } 82 | 83 | const search = PathFinder.search( 84 | creep.pos, { 85 | pos: spawn.pos, 86 | range: 1, 87 | }, { 88 | maxRooms: 1, 89 | }, 90 | ); 91 | if (config.visualizer.enabled && config.visualizer.showPathSearches) { 92 | visualizer.showSearch(search); 93 | } 94 | creep.move(creep.pos.getDirectionTo(search.path[0])); 95 | if (creep.pos.getRangeTo(spawn.pos) <= 1) { 96 | creep.attack(spawn); 97 | } else { 98 | const structures = creep.pos.findInRange(FIND_STRUCTURES, 1); 99 | creep.cancelOrder('attack'); 100 | creep.attack(structures[0]); 101 | } 102 | return true; 103 | }; 104 | -------------------------------------------------------------------------------- /src/role_powertransporter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * powertransporter is used to get power from the destroyed power bank 5 | * 6 | * Moves to the power and brings it back to the storage and destroys itself. 7 | */ 8 | 9 | // todo check routing reverse: is it memory.routing.reverse or memory.reverse 10 | 11 | roles.powertransporter = {}; 12 | roles.powertransporter.settings = { 13 | layoutString: 'MC', 14 | maxLayoutAmount: 20, 15 | fillTough: true, 16 | }; 17 | 18 | roles.powertransporter.moveHome = function(creep) { 19 | if (creep.carry.power > 0) { 20 | let storagePos; 21 | if (!creep.memory.storagePos) { 22 | const baseRoom = creep.memory.base; 23 | creep.log('moving back to baseRoom', baseRoom); 24 | storagePos = Game.rooms[baseRoom].memory.position.structure.storage[0]; 25 | creep.memory.storagePos = storagePos; 26 | } else { 27 | storagePos = creep.memory.storagePos; 28 | } 29 | creep.memory.routing.reverse = true; 30 | creep.memory.reverse = true; 31 | creep.moveTo(new RoomPosition(storagePos.x, storagePos.y, storagePos.roomName), { 32 | ignoreCreeps: true, 33 | }); 34 | // creep.moveToMy(new RoomPosition(storagePos.x, storagePos.y, storagePos.roomName), 2); 35 | return true; 36 | } 37 | return false; 38 | }; 39 | 40 | roles.powertransporter.moveToBankOrResource = function(creep) { 41 | const powerBank = creep.room.findPropertyFilter(FIND_STRUCTURES, 'structureType', [STRUCTURE_POWER_BANK]); 42 | if (powerBank.length > 0) { 43 | const range = creep.pos.getRangeTo(powerBank[0]); 44 | if (range > 3) { 45 | creep.moveTo(powerBank[0]); 46 | } 47 | return false; 48 | } 49 | const resource = creep.pos.findClosestByRangeDroppedPower(); 50 | if (resource === null) { 51 | if (creep.carry.power > 0) { 52 | return false; 53 | } else { 54 | creep.moveTo(25, 25); 55 | return false; 56 | } 57 | } 58 | return resource; 59 | }; 60 | 61 | roles.powertransporter.action = function(creep) { 62 | if (creep.carry.energy) { 63 | creep.drop(RESOURCE_ENERGY); 64 | } 65 | if (creep.memory.reverse && creep.inBase()) { 66 | creep.moveToMy(creep.room.storage.pos); 67 | const returnCode = creep.transfer(creep.room.storage, RESOURCE_POWER); 68 | if (returnCode === OK) { 69 | creep.log('Fill storage'); 70 | // todo-msc no suicide if we have recycle functions 71 | return Creep.recycleCreep(creep); 72 | } 73 | return true; 74 | } 75 | 76 | if (roles.powertransporter.moveHome(creep)) { 77 | return true; 78 | } 79 | const resource = roles.powertransporter.moveToBankOrResource(creep); 80 | if (!resource) { 81 | return true; 82 | } 83 | creep.moveTo(resource, { 84 | ignoreCreeps: true, 85 | }); 86 | const returnCode = creep.pickup(resource); 87 | if (returnCode === OK) { 88 | delete creep.memory.routing.reached; 89 | creep.memory.routing.reverse = true; 90 | creep.memory.reverse = true; 91 | } 92 | return true; 93 | }; 94 | -------------------------------------------------------------------------------- /src/prototype_creep_heal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Creep.prototype.isDamaged = function() { 4 | return this.hits / this.hitsMax; 5 | }; 6 | 7 | Creep.prototype.selfHeal = function() { 8 | if (!this.memory.canHeal) { 9 | this.memory.canHeal = this.getActiveBodyparts(HEAL) > 0; 10 | } 11 | if (this.memory.canHeal && this.isDamaged() < 1) { 12 | this.heal(this); 13 | } 14 | }; 15 | 16 | Creep.prototype.healMyCreeps = function() { 17 | const myCreeps = this.room.findMyCreepsToHeal(); 18 | if (myCreeps.length > 0) { 19 | this.say('heal', true); 20 | this.moveTo(myCreeps[0]); 21 | if (this.pos.getRangeTo(myCreeps[0]) <= 1) { 22 | this.heal(myCreeps[0]); 23 | } else { 24 | this.rangedHeal(myCreeps[0]); 25 | } 26 | return true; 27 | } 28 | return false; 29 | }; 30 | 31 | Creep.prototype.healAllyCreeps = function() { 32 | const allyCreeps = this.room.findAlliedCreepsToHeal(); 33 | if (allyCreeps.length > 0) { 34 | this.say('heal ally', true); 35 | this.moveTo(allyCreeps[0]); 36 | const range = this.pos.getRangeTo(allyCreeps[0]); 37 | if (range <= 1) { 38 | this.heal(allyCreeps[0]); 39 | } else { 40 | this.rangedHeal(allyCreeps[0]); 41 | } 42 | return true; 43 | } 44 | }; 45 | 46 | Creep.prototype.healCreep = function(range, myCreep) { 47 | if (range <= 1) { 48 | this.heal(myCreep); 49 | } else { 50 | this.moveTo(myCreep); 51 | this.rangedHeal(myCreep); 52 | } 53 | }; 54 | 55 | Creep.prototype.squadHeal = function() { 56 | let range; 57 | const creepToHeal = this.pos.findClosestByRange(FIND_MY_CREEPS, { 58 | filter: (object) => object.hits < object.hitsMax / 1.5, 59 | }); 60 | 61 | if (creepToHeal !== null) { 62 | range = this.pos.getRangeTo(creepToHeal); 63 | this.healCreep(range, creepToHeal); 64 | return true; 65 | } 66 | 67 | if (this.healClosestCreep(true)) { 68 | return true; 69 | } 70 | 71 | if (this.pos.isBorder(-1)) { 72 | this.moveTo(25, 25); 73 | return true; 74 | } 75 | 76 | const attacker = this.pos.findClosestByRangeSquadSiegeCreep(); 77 | if (attacker === null) { 78 | const cs = this.pos.findClosestByRange(FIND_CONSTRUCTION_SITES); 79 | this.moveTo(cs); 80 | return false; 81 | } 82 | this.moveTo(attacker); 83 | return false; 84 | }; 85 | 86 | Creep.prototype.healClosestCreep = function(andExit) { 87 | const myCreep = this.pos.findClosestByRange(FIND_MY_CREEPS, { 88 | filter: (o) => o.isDamaged() < 1, 89 | }); 90 | if (myCreep !== null) { 91 | this.say('heal', true); 92 | const range = this.pos.getRangeTo(myCreep); 93 | this.healCreep(range, myCreep); 94 | if (andExit) { 95 | if (myCreep.id === this.id) { 96 | this.say('exit'); 97 | const exit = this.pos.findClosestByRange(FIND_EXIT); 98 | this.moveTo(exit); 99 | } else { 100 | this.moveTo(myCreep); 101 | } 102 | } 103 | return true; 104 | } 105 | return false; 106 | }; 107 | -------------------------------------------------------------------------------- /src/role_atkeepermelee.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * atkeeper is used to kill Source Keeper (melee version) 5 | * 6 | * Attacks source keeper, move away when hits below 'threshold' 7 | * If no source keeper is available move to position where the next will spawn 8 | */ 9 | 10 | roles.atkeepermelee = {}; 11 | roles.atkeepermelee.settings = { 12 | layoutString: 'AMH', 13 | amount: [22, 25, 3], 14 | fillTough: false, 15 | maxLayoutAmount: 1, 16 | }; 17 | 18 | roles.atkeepermelee.preMove = function(creep) { 19 | creep.checkForRoutingReached(); 20 | if (creep.memory.canHeal && creep.isDamaged() < 1) { 21 | creep.heal(creep); 22 | } 23 | }; 24 | 25 | roles.atkeepermelee.action = function(creep) { 26 | // TODO Untested 27 | creep.spawnReplacement(); 28 | creep.setNextSpawn(); 29 | creep.memory.canHeal = creep.getActiveBodyparts(HEAL) > 0; 30 | 31 | if (creep.isDamaged() === 1) { 32 | creep.memory.damaged = false; 33 | } 34 | 35 | const moveToCenter = function(creep) { 36 | const center = new RoomPosition(25, 25, creep.memory.routing.targetRoom); 37 | return creep.moveTo(center, {ignoreCreeps: false, reusePath: 2}); 38 | }; 39 | 40 | if (creep.getActiveBodyparts(ATTACK) === 0) { 41 | creep.memory.damaged = true; 42 | } 43 | 44 | const healMove = function(creep, target) { 45 | const range = creep.pos.getRangeTo(target.pos); 46 | if (range > 5) { 47 | creep.moveToMy(target.pos); 48 | } else if (range > 2) { 49 | creep.moveTo(target.pos, {ignoreCreeps: false, reusePath: 2}); 50 | } else { 51 | creep.moveRandomWithin(target.pos, 1); 52 | if (target.structureType === 'keeperLair') { 53 | creep.selfHeal(); 54 | } 55 | } 56 | }; 57 | 58 | const attack = function(creep) { 59 | creep.say('attack'); 60 | // todo-msc cache target 61 | const lastTarget = Game.getObjectById(creep.room.memory.lastTarget); 62 | let target = (lastTarget && lastTarget.hits && lastTarget.hitsMax) ? lastTarget : creep.findClosestSourceKeeper(); 63 | if (target === null) { 64 | target = creep.findClosestEnemy() || creep.room.getNextSourceKeeperLair(); 65 | creep.room.memory.lastTarget = target.id; 66 | } 67 | if (target) { 68 | creep.room.memory.lastTarget = target.id; 69 | } 70 | healMove(creep, target); 71 | const returnValue = creep.attack(target); 72 | if (returnValue !== OK) { 73 | creep.selfHeal(); 74 | } 75 | return true; 76 | }; 77 | 78 | if (creep.room.name === creep.memory.routing.targetRoom) { 79 | if (creep.memory.damaged || creep.isDamaged() < 0.4 || (creep.getActiveBodyparts(ATTACK) === 0)) { 80 | if (creep.memory.canHeal) { 81 | creep.heal(creep); 82 | } 83 | creep.memory.damaged = true; 84 | creep.memory.canHeal = creep.getActiveBodyparts(HEAL) > 0; 85 | return moveToCenter(creep); 86 | } 87 | if (creep.room.keeperTeamReady()) { 88 | if (attack(creep)) { 89 | return true; 90 | } 91 | if (creep.isDamaged() <= 1 && creep.memory.canHeal) { 92 | creep.heal(creep); 93 | } 94 | } 95 | } 96 | return moveToCenter(creep); 97 | }; 98 | -------------------------------------------------------------------------------- /doc/Manual.md: -------------------------------------------------------------------------------- 1 | # Manual Commands 2 | 3 | The TooAngel bot provides various manual commands for administrative control and debugging. These commands are useful for testing specific behaviors, emergency interventions, or overriding automated decisions. 4 | 5 | ## Combat Operations 6 | 7 | ### Attacking a room with a single creep: 8 | 9 | ```javascript 10 | Game.rooms.W81N49.memory.queue.push({ 11 | role: 'autoattackmelee', 12 | routing: {targetRoom: 'W82N48'} 13 | }) 14 | ``` 15 | 16 | **Usage Notes:** 17 | - `Game.rooms.W81N49.memory.queue.push` - Queues creep spawning in the specified room 18 | - `role` - Specifies the creep role (can be any available role from the bot's role system) 19 | - `targetRoom` - Destination room for the creep's mission 20 | - Use this for targeted attacks or testing combat scenarios 21 | 22 | ## Territory Management 23 | 24 | ### Reserve a room's controller: 25 | 26 | ```javascript 27 | Game.rooms.W81N49.memory.queue.push({ 28 | role: 'reserver', 29 | routing: { 30 | targetRoom: 'W82N48', 31 | targetId: '5873bc0e11e3e4361b4d6fc3' 32 | } 33 | }) 34 | ``` 35 | 36 | **Usage Notes:** 37 | - `targetId` - The controller ID in the target room 38 | - This automatically triggers remote mining operations in the reserved room 39 | - Essential for expanding resource collection beyond controlled rooms 40 | 41 | ## Controller Signing 42 | 43 | ### Sign controller with default message: 44 | 45 | ```javascript 46 | Memory.rooms.E19N7.queue.push({ 47 | role: 'signer', 48 | routing: { 49 | targetRoom: 'E18N9', 50 | targetId: '5982ff1bb097071b4adc218c' 51 | } 52 | }) 53 | ``` 54 | 55 | ### Sign controller with custom message: 56 | 57 | ```javascript 58 | Memory.rooms.E19N7.queue.push({ 59 | role: 'signer', 60 | routing: { 61 | targetRoom: 'E18N9', 62 | targetId: '5982ff1bb097071b4adc218c' 63 | }, 64 | signText: 'Custom message here' 65 | }) 66 | ``` 67 | 68 | **Usage Notes:** 69 | - Default uses `config.info.signText` from configuration 70 | - Custom messages useful for diplomacy or territorial claims 71 | - Signs are visible on world map when hovering over controllers 72 | 73 | ## Direct Controller Operations 74 | 75 | ### Claim a controller: 76 | 77 | ```javascript 78 | Game.getObjectById('CreepId').claimController(Game.rooms.RoomName.controller) 79 | ``` 80 | 81 | ### Sign a controller directly: 82 | 83 | ```javascript 84 | Game.getObjectById('CreepId').signController( 85 | Game.rooms.RoomName.controller, 86 | "Your message here" 87 | ) 88 | ``` 89 | 90 | **Requirements:** 91 | - Creep must be adjacent to the controller 92 | - Claiming requires creep with CLAIM body parts 93 | - Signing can be done by any creep 94 | 95 | ## General Usage 96 | 97 | **Role Flexibility:** 98 | You can use the queue system to send any available creep role to specific targets: 99 | - `sourcer` - Energy harvesting operations 100 | - `carry` - Resource transportation 101 | - `defender` - Defensive operations 102 | - `builder` - Construction tasks 103 | - `repairer` - Maintenance operations 104 | 105 | **Future Features:** 106 | Squad-based attacks and coordinated operations are planned for future releases. 107 | -------------------------------------------------------------------------------- /src/role_upgrader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * upgrader upgrades the controller 5 | * 6 | * Gets the energy from the storage 7 | */ 8 | 9 | roles.upgrader = {}; 10 | roles.upgrader.settings = { 11 | param: ['controller.level'], 12 | prefixString: { 13 | 1: 'MCW', 14 | }, 15 | layoutString: { 16 | 1: 'W', 17 | }, 18 | maxLayoutAmount: { 19 | 1: 50, 20 | }, 21 | }; 22 | 23 | /** 24 | * updateSettings 25 | * 26 | * For RCL < 8: 27 | * One work part one energy per tick multiplied by config value with lifetime 28 | * So have at least a specific amount of energy in storage that the upgrader 29 | * can use. 30 | * Example with upgraderStorageFactor 2: 31 | * 6453 energy in storage are 2 workParts 32 | * 3000 energy will be put in the controller 33 | * 34 | * For RCL 8: 35 | * Linear scaling based on storage energy: 36 | * 10k storage → 1 WORK part (1 energy/tick) 37 | * 800k storage → 15 WORK parts (15 energy/tick, max) 38 | * 39 | * @param {object} room 40 | * @return {boolean|{maxLayoutAmount: number}} 41 | */ 42 | roles.upgrader.updateSettings = function(room) { 43 | if (!room.storage) { 44 | return false; 45 | } 46 | 47 | let workParts; 48 | 49 | if (room.controller.level === 8) { 50 | // Linear scaling for RCL 8: uses config.room.upgraderRcl8MinStorage → 1 WORK, config.room.upgraderRcl8MaxStorage → 15 WORK 51 | const minStorage = config.room.upgraderRcl8MinStorage; 52 | const maxStorage = config.room.upgraderRcl8MaxStorage; 53 | const minParts = 1; 54 | const maxParts = CONTROLLER_MAX_UPGRADE_PER_TICK; 55 | 56 | if (room.storage.store.energy <= minStorage) { 57 | workParts = minParts; 58 | } else if (room.storage.store.energy >= maxStorage) { 59 | workParts = maxParts; 60 | } else { 61 | workParts = minParts + Math.floor((room.storage.store.energy - minStorage) / (maxStorage - minStorage) * (maxParts - minParts)); 62 | } 63 | } else { 64 | // Keep existing formula for RCL < 8 65 | workParts = Math.floor((room.storage.store.energy + 1) / (CREEP_LIFE_TIME * config.room.upgraderStorageFactor)); 66 | } 67 | 68 | const maxLayoutAmount = Math.max(0, workParts - 1); 69 | if (config.debug.upgrader) { 70 | room.log(`upgrader updateSettings - storage.energy: ${room.storage.store.energy} upgraderStorageFactor: ${config.room.upgraderStorageFactor} workParts: ${workParts} maxLayoutAmount: ${maxLayoutAmount}`); 71 | } 72 | return { 73 | maxLayoutAmount: maxLayoutAmount, 74 | }; 75 | }; 76 | 77 | roles.upgrader.killPrevious = true; 78 | roles.upgrader.boostActions = ['upgradeController']; 79 | 80 | roles.upgrader.action = function(creep) { 81 | // Stop upgrading when trapped - let controller die 82 | if (Memory.trapped && Memory.trapped.isTrapped) { 83 | return true; 84 | } 85 | creep.mySignController(); 86 | creep.spawnReplacement(1); 87 | if (!creep.room.controller.isAboutToDowngrade()) { 88 | if (creep.room.isUnderAttack()) { 89 | return true; 90 | } 91 | if (creep.room.storage && creep.room.storage.isLow()) { 92 | return true; 93 | } 94 | } 95 | 96 | creep.upgradeController(creep.room.controller); 97 | creep.withdraw(creep.room.storage, RESOURCE_ENERGY); 98 | return true; 99 | }; 100 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at tooangel@tooangel.de. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TooAngel NPC / bot / source code for screeps 2 | 3 | [![CircleCI](https://circleci.com/gh/TooAngel/screeps.svg?style=svg)](https://circleci.com/gh/TooAngel/screeps) 4 | [![npm version](https://badge.fury.io/js/screeps-bot-tooangel.svg)](https://badge.fury.io/js/screeps-bot-tooangel) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/3c8ff1391c93ab7209af/maintainability)](https://codeclimate.com/github/TooAngel/screeps/maintainability) 6 | [![Discord](https://img.shields.io/discord/860665589738635336?color=7289da&label=Discord&logo=discord&logoColor=white)](https://discord.gg/RrGFHKb) 7 | 8 | 9 | This project is a codebase for [Screeps](https://screeps.com/), a strategy sandbox MMO game. The codebase is a fully automated player that covers all important features provided by the game. 10 | 11 | The TooAngel bot laid the groundwork for bots on private servers and pioneered the full automation concept. It was the first fully automated open source codebase and introduced the concept of community-driven merge processes as well as fully automated bot deployment. 12 | 13 | Pull Requests are automatically merged ([World Driven](https://www.worlddriven.org)) and deployed to the 14 | [Screeps TooAngel account](https://www.screeps.com). 15 | 16 | ## Use cases 17 | 18 | There are different occasions where you get into contact with the TooAngel NPC / bot / source code. 19 | 20 | - [As NPC on the public server](doc/NPC.md) 21 | - [Deployed as bot on a private server](doc/Bot.md) 22 | - [Using it as code base](doc/CodeBase.md) 23 | 24 | ## Contributing 25 | 26 | This project welcomes all kinds of contributions! Whether you're reporting issues, suggesting features, or submitting pull requests, your input helps improve the bot. 27 | 28 | Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details about the automated merge process and how to get involved. 29 | 30 | The codebase includes many innovative ideas and solutions developed through practical gameplay experience. While some code was written quickly during active gameplay situations, the project continues to evolve and improve with community contributions. 31 | 32 | ## Features 33 | 34 | ### Core Automation 35 | - [Automatic base building](doc/BaseBuilding.md) - Smart room layout and construction 36 | - Remote harvesting - Automated resource collection from external rooms 37 | - Room extension and expansion management 38 | - Reviving attacked rooms automatically 39 | 40 | ### Resource Management 41 | - [Mineral handling, harvesting, market, reactions and boosting](doc/Mineral.md) 42 | - Power and commodity harvesting 43 | - Automated market trading 44 | 45 | ### AI Systems 46 | - [Trapped scenario detection](doc/Design.md#trapped-detection-system) - Detects when bot is imprisoned by hostile players 47 | - [Diplomatic module for retaliations](doc/Diplomacy.md) - Reputation-based player interactions 48 | - [Quest system](doc/Quests.md) - Interactive challenges for other players 49 | 50 | ### Development Tools 51 | - [Layout visualization](doc/Visualization.md) - Visual debugging tools 52 | - [Manual commands](doc/Manual.md) - Administrative controls 53 | - [Testing framework](doc/Testing.md) - Automated testing system 54 | 55 | 56 | ## Design 57 | 58 | [More details of the AI design](doc/Design.md) 59 | 60 | ## Links 61 | 62 | - [Game Docs](https://docs.screeps.com/) 63 | - [API Docs](https://docs.screeps.com/api/) 64 | -------------------------------------------------------------------------------- /doc/Diplomacy.md: -------------------------------------------------------------------------------- 1 | # Diplomacy System 2 | 3 | The TooAngel bot features a sophisticated reputation-based diplomacy system that dynamically adjusts behavior based on interactions with other players. 4 | 5 | ## Reputation Mechanics 6 | 7 | ### Core Concept 8 | A `reputation` score is maintained for each player the bot encounters. This score influences all future interactions and determines the bot's behavioral responses to that player. 9 | 10 | ### Reputation Factors 11 | 12 | #### Positive Actions (Increase Reputation) 13 | - **Resource Transfers**: Sending resources via terminal increases reputation based on market value 14 | - **Quest Completion**: Successfully solving quests provides significant reputation bonuses 15 | - **Peaceful Coexistence**: Respecting room boundaries and avoiding hostile actions 16 | - **Collaborative Behavior**: Supporting mutual objectives and peaceful interactions 17 | 18 | #### Negative Actions (Decrease Reputation) 19 | - **Hostile Creeps**: Sending armed creeps into controlled rooms 20 | - **Structure Destruction**: Attacking or destroying spawns, towers, and other structures 21 | - **Nuclear Attacks**: Launching nukes results in severe reputation penalties 22 | - **Resource Theft**: Attempting to steal resources or disrupt operations 23 | 24 | ## Behavioral Responses 25 | 26 | ### Positive Reputation Benefits 27 | - **Safe Passage**: Player creeps can move through reserved/controlled rooms without retaliation 28 | - **Resource Sharing**: Access to power banks and other valuable resources in bot-controlled areas 29 | - **Quest Priority**: Higher chance of receiving valuable or interesting quests 30 | - **Defensive Assistance**: Bot may provide defensive support in certain situations 31 | 32 | ### Negative Reputation Consequences 33 | - **Room Harassment**: Bot becomes annoying presence in player's reserved rooms 34 | - **Active Hostility**: Direct attacks on player's rooms and structures 35 | - **Nuclear Retaliation**: Severe negative reputation may trigger nuclear responses 36 | - **Resource Denial**: Blocked access to shared resources and opportunities 37 | 38 | ## Reputation Levels 39 | 40 | The system operates on multiple reputation thresholds that trigger different behavioral states: 41 | 42 | ### High Positive (Ally Status) 43 | - Full cooperation and resource sharing 44 | - Priority quest offerings 45 | - Defensive pacts and mutual protection 46 | 47 | ### Moderate Positive (Friendly) 48 | - Safe passage through territories 49 | - Basic resource sharing opportunities 50 | - Standard quest interactions 51 | 52 | ### Neutral (Default) 53 | - Standard defensive behavior 54 | - Limited interactions based on immediate context 55 | 56 | ### Moderate Negative (Hostile) 57 | - Increased territorial aggression 58 | - Resource competition and denial 59 | - Retaliatory strikes for continued hostility 60 | 61 | ### High Negative (Enemy Status) 62 | - Full warfare protocols activated 63 | - Nuclear deterrent deployment 64 | - Persistent harassment and attack patterns 65 | 66 | ## Integration with Quest System 67 | 68 | The diplomacy system is tightly integrated with the [Quest system](Quests.md), where reputation determines: 69 | - Quest difficulty and rewards 70 | - Quest availability and types 71 | - Trust levels for collaborative missions 72 | - Penalty systems for quest failures 73 | 74 | For detailed API information about reputation queries and diplomatic communications, see [API.md](API.md). -------------------------------------------------------------------------------- /src/role_structurer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * structurer is called when there are structures in a reserved room 5 | * 6 | * Checks the paths for blocking structures => dismantles them 7 | * Searches for other structures => dismantles them 8 | * If there is 'threshold' energy below structurer => call a carry 9 | */ 10 | 11 | roles.structurer = {}; 12 | roles.structurer.boostActions = ['dismantle']; 13 | 14 | roles.structurer.settings = { 15 | layoutString: 'MW', 16 | amount: [5, 5], 17 | }; 18 | 19 | 20 | /** 21 | * dismantleStructure - Dismantles structures in moving direction 22 | * 23 | * @param {object} creep - The creep object 24 | * @param {object} structure - The structure to check and dismantle 25 | * @return {boolean} - Dismantling 26 | **/ 27 | function dismantleStructure(creep, structure) { 28 | if (structure.structureType === STRUCTURE_ROAD) { 29 | return false; 30 | } 31 | if (structure.structureType === STRUCTURE_RAMPART && structure.my) { 32 | return false; 33 | } 34 | 35 | creep.say('dismantle'); 36 | creep.dismantle(structure); 37 | return true; 38 | } 39 | 40 | 41 | /** 42 | * findAndDismantleStructure - Finds and dismantles structures 43 | * 44 | * @param {object} creep - The creep object 45 | * @param {object} directions - The directions object 46 | * @return {void} 47 | **/ 48 | function findAndDismantleStructure(creep, directions) { 49 | if (!directions || !directions.forwardDirection) { 50 | return; 51 | } 52 | const posForward = creep.pos.getAdjacentPosition(directions.forwardDirection); 53 | const structures = posForward.lookFor(LOOK_STRUCTURES); 54 | for (const structure of structures) { 55 | if (dismantleStructure(creep, structure)) { 56 | break; 57 | } 58 | } 59 | return; 60 | } 61 | 62 | /** 63 | * preMoveTargetRoom - preMove in targetRoom 64 | * 65 | * @param {object} creep - The creep object 66 | * @param {object} directions - The directions object 67 | * @return {void} 68 | **/ 69 | function preMoveTargetRoom(creep, directions) { 70 | if (creep.room.name !== creep.memory.routing.targetRoom) { 71 | return; 72 | } 73 | 74 | const target = Game.getObjectById(creep.memory.routing.targetId); 75 | if (target === null) { 76 | creep.log('Invalid target'); 77 | delete creep.memory.routing.targetId; 78 | } 79 | 80 | findAndDismantleStructure(creep, directions); 81 | } 82 | 83 | roles.structurer.preMove = function(creep, directions) { 84 | creep.creepLog(`preMove: targetId: ${creep.memory.routing.targetId}`); 85 | preMoveTargetRoom(creep, directions); 86 | 87 | // Routing would end within the wall - this is the fix for that 88 | if (creep.memory.routing.targetId && creep.room.name === creep.memory.routing.targetRoom) { 89 | const target = Game.getObjectById(creep.memory.routing.targetId); 90 | if (target === null) { 91 | delete creep.memory.routing.targetId; 92 | return true; 93 | } 94 | if (creep.pos.getRangeTo(target.pos) <= 1) { 95 | creep.memory.routing.reached = true; 96 | } 97 | } 98 | }; 99 | 100 | roles.structurer.action = function(creep) { 101 | creep.creepLog('action'); 102 | if (!creep.room.controller || !creep.room.controller.my) { 103 | const structure = creep.pos.findClosestByRangeStructureToDestroy(); 104 | creep.dismantle(structure); 105 | } 106 | 107 | creep.spawnReplacement(1); 108 | creep.handleStructurer(); 109 | return true; 110 | }; 111 | -------------------------------------------------------------------------------- /src/role_attackunreserve.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * Called to defend external rooms 5 | * 6 | * Fights against hostile creeps 7 | */ 8 | 9 | roles.attackunreserve = {}; 10 | roles.attackunreserve.boostActions = ['rangedAttack', 'heal']; 11 | 12 | roles.attackunreserve.settings = { 13 | prefixString: 'MMMMRRHH', 14 | layoutString: 'AM', 15 | }; 16 | 17 | roles.attackunreserve.preMove = function(creep) { 18 | if (creep.hits < 0.5 * creep.hitsMax) { 19 | creep.memory.routing.reverse = true; 20 | creep.memory.routing.reached = false; 21 | const inRange = creep.pos.findInRange(FIND_HOSTILE_CREEPS, 3); 22 | if (inRange.length > 0) { 23 | creep.creepLog('Attack creep in range'); 24 | attack(creep, inRange[0]); 25 | } 26 | return false; 27 | } 28 | if (creep.hits > 0.75 * creep.hitsMax) { 29 | creep.memory.routing.reverse = false; 30 | return false; 31 | } 32 | creep.selfHeal(); 33 | if (!creep.inMyRoom()) { 34 | let targets = creep.pos.findHostileStructuresInRangedAttackRange(); 35 | if (targets.length === 0) { 36 | targets = creep.pos.findInRangeStructures(FIND_STRUCTURES, 1, [STRUCTURE_WALL, STRUCTURE_RAMPART]); 37 | } 38 | creep.rangeAttackOutsideOfMyRooms(targets); 39 | } 40 | }; 41 | 42 | /** 43 | * getFilterForBodyPart - gets a filter for a given bodyPart 44 | * 45 | * @param {string} bodyPart - The body part name 46 | * @return {function} - The filter function for that body part 47 | **/ 48 | function getFilterForBodyPart(bodyPart) { 49 | return (item) => { 50 | for (const part of item.body) { 51 | if (part.type === bodyPart) { 52 | return true; 53 | } 54 | } 55 | return false; 56 | }; 57 | } 58 | 59 | /** 60 | * attach - attacks the target 61 | * 62 | * @param {object} creep - The attacker 63 | * @param {object} target - The target 64 | * @return {boolean} - Returns true :-) 65 | **/ 66 | function attack(creep, target) { 67 | // TODO needs to be changed to some of our moveTo methods, preventing from 68 | // exiting the room 69 | if (target.pos.x > 0 && target.pos.x < 49 && target.pos.y > 0 && target.pos.y < 49) { 70 | creep.moveTo(target.pos); 71 | } 72 | creep.rangedAttack(target); 73 | creep.attack(target); 74 | return true; 75 | } 76 | 77 | roles.attackunreserve.action = function(creep) { 78 | creep.notifyWhenAttacked(false); 79 | creep.selfHeal(); 80 | 81 | const inRange = creep.pos.findInRange(FIND_HOSTILE_CREEPS, 3); 82 | if (inRange.length > 0) { 83 | creep.creepLog('Attack creep in range'); 84 | return attack(creep, inRange[0]); 85 | } 86 | 87 | const parts = [CLAIM, WORK, CARRY]; 88 | for (const part of parts) { 89 | const hostileCreepsWithPart = creep.pos.findClosestByRange(FIND_HOSTILE_CREEPS, {filter: getFilterForBodyPart(part)}); 90 | if (hostileCreepsWithPart) { 91 | return attack(creep, hostileCreepsWithPart); 92 | } 93 | } 94 | 95 | const hostileCreep = creep.pos.findClosestByRange(FIND_HOSTILE_CREEPS); 96 | if (hostileCreep) { 97 | creep.creepLog(`attack other hostile creeps`); 98 | return attack(creep, hostileCreep); 99 | } 100 | 101 | const hostileStructure = creep.pos.findClosestByRange(FIND_HOSTILE_STRUCTURES); 102 | if (hostileStructure) { 103 | creep.creepLog(`attack other hostile structures`); 104 | return attack(creep, hostileStructure); 105 | } 106 | 107 | const structure = creep.pos.findClosestByRange(FIND_STRUCTURES); 108 | if (structure) { 109 | return attack(creep, structure); 110 | } 111 | this.log('No target found'); 112 | return true; 113 | }; 114 | -------------------------------------------------------------------------------- /src/prototype_room.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * The data property represent the current data of the room stored on the heap 5 | */ 6 | Object.defineProperty(Room.prototype, 'data', { 7 | get() { 8 | if (!global.data.rooms[this.name]) { 9 | const data = { 10 | routing: {}, 11 | positions: { 12 | creep: {}, 13 | structure: {}, 14 | }, 15 | }; 16 | if (this.isMy()) { 17 | data.positions = this.memory.position || {}; 18 | // TODO do not store costMatrix in memory, this can be generated from positions 19 | data.costMatrix = PathFinder.CostMatrix.deserialize(this.memory.costMatrix); 20 | data.routing = {}; 21 | if (this.memory.routing) { 22 | for (const pathName of Object.keys(this.memory.routing)) { 23 | const path = Room.stringToPath(this.memory.routing[pathName].path); 24 | data.routing[pathName] = { 25 | path: path, 26 | created: this.memory.routing[pathName].created, 27 | fixed: this.memory.routing[pathName].fixed, 28 | name: this.memory.routing[pathName].name, 29 | }; 30 | } 31 | } 32 | } 33 | global.data.rooms[this.name] = data; 34 | } 35 | return global.data.rooms[this.name]; 36 | }, 37 | }); 38 | 39 | Room.prototype.executeEveryTicks = function(ticks) { 40 | const timer = (ticks > 3000) ? Game.time - Memory.time + 1 : 0; 41 | let execute = false; 42 | if (this.controller) { 43 | execute = (timer > 1) ? (Game.time + this.controller.pos.x + this.controller.pos.y) % ticks < timer : (Game.time + this.controller.pos.x + this.controller.pos.y) % ticks === 0; 44 | } else { 45 | execute = (timer > 1) ? (Game.time % ticks) < timer : (Game.time % ticks) === 0; 46 | } 47 | return execute; 48 | }; 49 | 50 | /** 51 | * updateBasicData - Updates basic room data 52 | * - Sets the number of sources 53 | * - Sets the controller id 54 | * - Sets the hostile count 55 | * 56 | * @param {object} room - The room to init 57 | * @return {void} 58 | **/ 59 | function updateBasicData(room) { 60 | if (room.data.sources === undefined) { 61 | room.data.sources = room.findSources().length; 62 | } 63 | if (room.data.controllerId === undefined) { 64 | room.data.controllerId = false; 65 | if (room.controller) { 66 | room.data.controllerId = room.controller.id; 67 | if (!room.data.mineral) { 68 | const minerals = room.findMinerals(); 69 | room.data.mineral = minerals[0].mineralType; 70 | } 71 | } 72 | } 73 | room.data.hostileCreepCount = room.find(FIND_HOSTILE_CREEPS).length; 74 | } 75 | 76 | Room.prototype.isMy = function() { 77 | return !!(this.controller && this.controller.my); 78 | }; 79 | 80 | Room.prototype.handle = function() { 81 | updateBasicData(this); 82 | if (this.isMy()) { 83 | this.myHandleRoom(); 84 | return true; 85 | } 86 | this.externalHandleRoom(); 87 | return false; 88 | }; 89 | 90 | Room.prototype.execute = function() { 91 | try { 92 | const returnCode = this.handle(); 93 | for (const creep of this.findMyCreeps()) { 94 | creep.handle(); 95 | } 96 | return returnCode; 97 | } catch (err) { 98 | this.log('Executing room failed: ' + this.name + ' ' + err + ' ' + err.stack); 99 | Game.notify('Executing room failed: ' + this.name + ' ' + err + ' ' + err.stack, 30); 100 | return false; 101 | } finally { 102 | this.data.lastSeen = Game.time; 103 | if (this.isMy()) { 104 | this.memory.lastSeen = Game.time; 105 | } 106 | } 107 | }; 108 | -------------------------------------------------------------------------------- /src/brain_memory_market.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const {debugLog} = require('./logging'); 5 | const {addToReputation} = require('./diplomacy'); 6 | const {checkQuestForAcceptance} = require('./quests_host'); 7 | const {checkAppliedQuestForAcceptance} = require('./quests_player'); 8 | 9 | brain.setMarketOrders = function() { 10 | Memory.orders = {}; 11 | Memory.orders[ORDER_BUY] = {}; 12 | Memory.orders[ORDER_SELL] = {}; 13 | for (const order of Game.market.getAllOrders()) { 14 | if (Memory.myRooms.includes(order.roomName)) { 15 | continue; 16 | } 17 | let category = Memory.orders[order.type][order.resourceType]; 18 | if (!category) { 19 | Memory.orders[order.type][order.resourceType] = category = { 20 | min: order.price, 21 | max: order.price, 22 | totalPrice: 0, 23 | totalAmount: 0, 24 | orders: [], 25 | }; 26 | } 27 | category.min = Math.min(category.min, order.price); 28 | category.max = Math.max(category.max, order.price); 29 | category.totalPrice += order.price * order.remainingAmount; 30 | category.totalAmount += order.remainingAmount; 31 | category.orders.push(order); 32 | } 33 | }; 34 | 35 | brain.getMarketOrderAverage = (type, resource) => Memory.orders[type][resource] && Memory.orders[type][resource].totalPrice ? Memory.orders[type][resource].totalPrice / Memory.orders[type][resource].totalAmount : null; 36 | 37 | brain.getMarketOrder = (type, resource, property) => Memory.orders[type][resource] && Memory.orders[type][resource][property] ? Memory.orders[type][resource][property] : null; 38 | 39 | brain.buyPower = function() { 40 | if (!config.market.buyPower) { 41 | return false; 42 | } 43 | debugLog('brain', 'buyPower'); 44 | const filterRoomPowerSpawn = (r) => Game.rooms[r] && Game.rooms[r].controller.level === 8 && Memory.rooms[r] && Memory.rooms[r].constants && !!Memory.rooms[r].constants.powerSpawn; 45 | const roomName = _.first(_.filter(_.shuffle(Memory.myRooms), filterRoomPowerSpawn)) || false; 46 | // low cash 47 | if (Game.market.credits < config.market.minCredits || !roomName) { 48 | return false; 49 | } 50 | // deal one order 51 | const deal = function(item) { 52 | if (item.price < 1) { 53 | return Game.market.deal(item.id, 1000, roomName); 54 | } 55 | return false; 56 | }; 57 | // if no cooldown 58 | if (Game.rooms[roomName].terminal && !Game.rooms[roomName].terminal.cooldown) { 59 | return _.map(Game.market.getAllOrders({type: ORDER_SELL, resourceType: RESOURCE_POWER}), deal); 60 | } 61 | return false; 62 | }; 63 | 64 | brain.handleIncomingTransactionsByUser = function(transaction) { 65 | const sender = transaction.sender.username; 66 | if (sender === Memory.username) { 67 | return false; 68 | } 69 | const price = brain.getMarketOrder(ORDER_SELL, transaction.resourceType, 'min') || brain.getMarketOrder(ORDER_BUY, transaction.resourceType, 'max') || 1; 70 | const value = transaction.amount * price; 71 | const room = Game.rooms[transaction.to]; 72 | room.debugLog('market', `Incoming transaction from ${sender}[${transaction.from}] ${transaction.amount} ${transaction.resourceType} market price: ${price}`); 73 | addToReputation(sender, value); 74 | return true; 75 | }; 76 | 77 | brain.handleIncomingTransactionsTimeFilter = (object) => { 78 | // TODO save last checked value, so we will see all transactions even in case of CPU-skipped ticks 79 | return object.time >= Game.time - 1; 80 | }; 81 | 82 | brain.handleIncomingTransactions = function() { 83 | const transactions = Game.market.incomingTransactions; 84 | const current = _.filter(transactions, brain.handleIncomingTransactionsTimeFilter); 85 | 86 | for (const transaction of current) { 87 | if (transaction.sender) { 88 | brain.handleIncomingTransactionsByUser(transaction); 89 | } 90 | checkQuestForAcceptance(transaction); 91 | checkAppliedQuestForAcceptance(transaction); 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /test/test_setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Shared test setup for all test files. 3 | * This file sets up global mocks and loads the main module once. 4 | * All test files should require this instead of setting up globals themselves. 5 | */ 6 | 7 | // Only run setup once (check if already initialized) 8 | if (!global._testSetupComplete) { 9 | const variables = require('@screeps/common/lib/constants'); // eslint-disable-line global-require 10 | 11 | for (const variableName of Object.keys(variables)) { 12 | global[variableName] = variables[variableName]; 13 | } 14 | 15 | global.Room = function(name, energyAvailable) { 16 | this.name = name; 17 | this.energyAvailable = energyAvailable; 18 | this.energyCapacityAvailable = energyAvailable; 19 | this.memory = { 20 | energyStats: {}, 21 | active: true, 22 | }; 23 | this.controller = { 24 | level: 1, 25 | }; 26 | this.debugLog = () => {}; 27 | this.find = () => []; 28 | }; 29 | global.RoomObject = function() {}; 30 | global.RoomPosition = function(x, y, roomName) { 31 | this.x = x; 32 | this.y = y; 33 | this.roomName = roomName; 34 | }; 35 | global.Creep = function(role) { 36 | this.role = role; 37 | }; 38 | global.Structure = function() {}; 39 | global.StructureController = function() {}; 40 | global.StructureStorage = function() {}; 41 | global._ = require('lodash'); // eslint-disable-line global-require 42 | global.Game = new function() { 43 | this.time = 1; 44 | this.cpu = { 45 | getUsed: () => {}, 46 | }; 47 | this.gcl = { 48 | level: 10, 49 | }; 50 | }; 51 | global.Memory = new function() {}; 52 | 53 | require('../src/main'); // eslint-disable-line global-require 54 | 55 | global._testSetupComplete = true; 56 | } 57 | 58 | /** 59 | * Creates a mock storage using StructureStorage prototype 60 | * @param {object} options - Configuration options 61 | * @return {object} Storage object with proper prototype 62 | */ 63 | function createStorage(options = {}) { 64 | const storage = Object.create(StructureStorage.prototype); 65 | storage.my = options.my !== undefined ? options.my : true; 66 | storage.pos = {x: 25, y: 26, roomName: options.roomName || 'W1N1'}; 67 | storage.store = { 68 | energy: options.energy !== undefined ? options.energy : 100000, 69 | }; 70 | return storage; 71 | } 72 | 73 | /** 74 | * Creates a mock room with proper prototype chain 75 | * @param {object} options - Configuration options 76 | * @return {object} Room object 77 | */ 78 | function createRoom(options = {}) { 79 | const energyCapacity = options.energyCapacityAvailable || 5000; 80 | const room = new Room(options.name || 'W1N1', energyCapacity); 81 | room.energyCapacityAvailable = energyCapacity; 82 | room.memory.misplacedSpawn = options.misplacedSpawn || false; 83 | room.memory.active = options.active !== undefined ? options.active : true; 84 | 85 | if (options.hasStorage !== false) { 86 | room.storage = createStorage({ 87 | my: options.storageMy, 88 | energy: options.storageEnergy, 89 | roomName: room.name, 90 | }); 91 | } 92 | 93 | return room; 94 | } 95 | 96 | /** 97 | * Creates a mock creep for testing 98 | * @param {object} options - Configuration options 99 | * @return {object} Mock creep object 100 | */ 101 | function createCreep(options = {}) { 102 | const room = options.room || createRoom(); 103 | return { 104 | name: options.name || 'test-creep', 105 | room: room, 106 | pos: { 107 | x: options.x || 25, 108 | y: options.y || 25, 109 | roomName: room.name, 110 | getRangeTo: options.getRangeTo || (() => 5), 111 | }, 112 | store: { 113 | energy: options.energy || 0, 114 | }, 115 | withdraw: options.withdraw || (() => OK), 116 | moveToMy: options.moveToMy || (() => OK), 117 | }; 118 | } 119 | 120 | module.exports = { 121 | createStorage, 122 | createRoom, 123 | createCreep, 124 | }; 125 | -------------------------------------------------------------------------------- /src/role_scout.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * scout moves around to provide visibility 5 | */ 6 | 7 | roles.scout = {}; 8 | roles.scout.settings = { 9 | layoutString: 'M', 10 | amount: [1], 11 | maxLayoutAmount: 1, 12 | }; 13 | 14 | /** 15 | * move - moves the creep to the nextRoom 16 | * 17 | * @param {object} creep 18 | */ 19 | function move(creep) { 20 | let path = creep.memory.path; 21 | if (path) { 22 | // TODO is seems like the creep can't reuse the path, needs to be checked 23 | const moveResult = creep.moveByPath(path); 24 | if (moveResult === OK) { 25 | creep.memory.incompleteCount = 0; 26 | return; 27 | } 28 | if (moveResult !== ERR_NOT_FOUND && moveResult !== ERR_INVALID_ARGS) { 29 | return; 30 | } 31 | // creep.log(moveResult); 32 | } 33 | let incompleteCount = creep.memory.incompleteCount; 34 | if (!incompleteCount) { 35 | incompleteCount = 1; 36 | } else { 37 | ++incompleteCount; 38 | } 39 | if (incompleteCount > 25) { 40 | incompleteCount = 0; 41 | creep.data.nextRoom = getNextRoom(creep); 42 | } 43 | creep.memory.incompleteCount = incompleteCount; 44 | const targetPosObject = new RoomPosition(25, 25, creep.data.nextRoom); 45 | // creep.log('Searching for path'); 46 | const search = PathFinder.search( 47 | creep.pos, 48 | { 49 | pos: targetPosObject, 50 | range: 20, 51 | }, 52 | { 53 | roomCallback: creep.room.getCostMatrixCallback(targetPosObject, false, true, true), 54 | }, 55 | ); 56 | creep.memory.path = path = search.path; 57 | const moveResult = creep.moveByPath(path); 58 | if (moveResult === OK) { 59 | creep.memory.incompleteCount = 0; 60 | } 61 | } 62 | 63 | /** 64 | * getNextRoom 65 | * 66 | * @param {object} creep 67 | * @return {string} 68 | */ 69 | function getNextRoom(creep) { 70 | const exits = Game.map.describeExits(creep.room.name); 71 | 72 | const rooms = Object.keys(exits).map((direction) => { 73 | return {name: exits[direction], direction: parseInt(direction, 10)}; 74 | }); 75 | 76 | rooms.sort(() => Math.random() - 0.5); 77 | let nextRoom = rooms[0]; 78 | let lastSeen = (global.data.rooms[nextRoom.name] || {}).lastSeen; 79 | for (const room of rooms) { 80 | const roomLastSeen = (global.data.rooms[room.name] || {}).lastSeen; 81 | 82 | const exitPositions = creep.room.find(room.direction); 83 | // Check if exit is blocked by indestructible walls 84 | // Newbie/novice zone walls have a decayTime property and hitsMax, but cannot be passed 85 | const isExitBlocked = exitPositions.every((exitPos) => { 86 | const structures = creep.room.lookForAt(LOOK_STRUCTURES, exitPos); 87 | return structures.some((s) => s.structureType === STRUCTURE_WALL && (!s.hitsMax || s.decayTime)); 88 | }); 89 | if (isExitBlocked) { 90 | creep.log(`Exit blocked by indestructable walls`); 91 | continue; 92 | } 93 | const roomCallback = (roomName) => { 94 | const room = Game.rooms[roomName]; 95 | const costMatrix = new PathFinder.CostMatrix(); 96 | if (room) { 97 | const structures = room.findStructures(); 98 | room.setCostMatrixStructures(costMatrix, structures, 255); 99 | } 100 | return costMatrix; 101 | }; 102 | const search = PathFinder.search(creep.pos, exitPositions[0], { 103 | maxRooms: 1, 104 | roomCallback: roomCallback, 105 | }); 106 | if (search.incomplete) { 107 | creep.log(`Skipping ${room.name} - no path`); 108 | continue; 109 | } 110 | if ((lastSeen && !roomLastSeen) || (lastSeen > roomLastSeen)) { 111 | nextRoom = room; 112 | lastSeen = roomLastSeen; 113 | } 114 | } 115 | return nextRoom.name; 116 | } 117 | 118 | /** 119 | * explore - follow the unseen or latest `lastSeen` rooms 120 | * 121 | * @param {object} creep 122 | */ 123 | function explore(creep) { 124 | if (!creep.data.nextRoom) { 125 | creep.data.nextRoom = getNextRoom(creep); 126 | } 127 | if (creep.room.name === creep.data.nextRoom) { 128 | creep.data.nextRoom = getNextRoom(creep); 129 | } 130 | move(creep); 131 | } 132 | 133 | roles.scout.action = function(creep) { 134 | creep.notifyWhenAttacked(false); 135 | explore(creep); 136 | return true; 137 | }; 138 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const {startAttack} = require('./diplomacy'); 2 | 3 | /** 4 | * This module is used to allow player easy access to different actions on the NPC 5 | * like asking for reports, triggering attacks, ... 6 | */ 7 | 8 | global.utils = { 9 | /** 10 | * Show queue 11 | * 12 | * @param {string} roomName 13 | */ 14 | showQueue: (roomName) => { 15 | const memory = Memory.rooms[roomName]; 16 | if (!memory) { 17 | console.log('room not found'); 18 | return; 19 | } 20 | console.log(memory.queue.map((item) => item.role)); 21 | }, 22 | /** 23 | * Attack player 24 | * 25 | * @param {string} playerName 26 | */ 27 | attack: (playerName) => { 28 | const player = Memory.players[playerName]; 29 | if (!player) { 30 | console.log('player not found'); 31 | return; 32 | } 33 | startAttack(player); 34 | }, 35 | /** 36 | * Print out known players 37 | */ 38 | printPlayers: function() { 39 | for (const playerKey of Object.keys(Memory.players)) { 40 | const player = Memory.players[playerKey]; 41 | console.log(`${player.name} rooms: ${Object.keys(player.rooms).length} level: ${player.level} counter: ${player.counter} reputation: ${player.reputation}`); 42 | } 43 | // console.log(JSON.stringify(Memory.players, null, 2)); 44 | }, 45 | 46 | /** 47 | * roomCheck, checks for occupied rooms 48 | */ 49 | roomCheck: function() { 50 | for (const roomName in Memory.rooms) { 51 | if (Memory.rooms[roomName].state === 'Occupied') { 52 | console.log(`${roomName} ${global.data.rooms[roomName].player}`); 53 | } 54 | } 55 | console.log('roomCheck - done'); 56 | }, 57 | 58 | /** 59 | * Prinst out the status of all terminals 60 | */ 61 | terminals: function() { 62 | console.log('Terminals:'); 63 | for (const roomName of Memory.myRooms) { 64 | const room = Game.rooms[roomName]; 65 | if (room.terminal) { 66 | console.log(`${roomName} ${JSON.stringify(room.terminal.store)}`); 67 | } 68 | } 69 | console.log('terminals - done'); 70 | }, 71 | 72 | 73 | /** 74 | * Lists the number of constructionSites per rooms 75 | */ 76 | constructionSiteStats: function() { 77 | const aggregate = function(result, value) { 78 | result[value.pos.roomName] = (result[value.pos.roomName] || (result[value.pos.roomName] = 0)) + 1; 79 | return result; 80 | }; 81 | const resultReduce = _.reduce(Game.constructionSites, aggregate, {}); 82 | console.log(JSON.stringify(resultReduce)); 83 | console.log('constructionSiteStats - done'); 84 | }, 85 | 86 | /** 87 | * Prints the memory usage for the different keys 88 | */ 89 | memory: function() { 90 | for (const keys of Object.keys(Memory)) { 91 | console.log(keys, JSON.stringify(Memory[keys]).length); 92 | } 93 | console.log('memory - done'); 94 | }, 95 | 96 | /** 97 | * Prints the memory usage for the different rooms 98 | */ 99 | memoryRooms: function() { 100 | for (const keys of Object.keys(Memory.rooms)) { 101 | console.log(keys, JSON.stringify(Memory.rooms[keys]).length); 102 | } 103 | console.log('memoryRooms - done'); 104 | }, 105 | 106 | /** 107 | * Prints the memory usage for a specific room 108 | * 109 | * @param {string} roomName 110 | */ 111 | memoryRoom: function(roomName) { 112 | for (const keys of Object.keys(Memory.rooms[roomName])) { 113 | console.log(keys, JSON.stringify(Memory.rooms[roomName][keys]).length); 114 | } 115 | console.log('memoryRoom - done'); 116 | }, 117 | 118 | /** 119 | * Prints all reserved rooms 120 | */ 121 | showReservedRooms: function() { 122 | for (const roomName of Object.keys(Memory.rooms)) { 123 | const room = Memory.rooms[roomName]; 124 | if (room.state === 'Reserved') { 125 | console.log(roomName, JSON.stringify(room.reservation)); 126 | } 127 | } 128 | console.log('showReservedRooms - done'); 129 | }, 130 | 131 | /** 132 | * Shows the queue aggregated by role 133 | * 134 | * @param {string} roomName 135 | */ 136 | queueCheck: function(roomName) { 137 | const found = _.countBy(Memory.rooms[roomName].queue, (object) => object.role); 138 | console.log(JSON.stringify(found)); 139 | }, 140 | }; 141 | -------------------------------------------------------------------------------- /src/prototype_creep_clean.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Creep.prototype.handleStructurer = function() { 4 | if (!this.memory.routing.targetId) { 5 | return this.cleanSetTargetId(); 6 | } 7 | 8 | const structure = Game.getObjectById(this.memory.routing.targetId); 9 | if (structure === null) { 10 | delete this.memory.routing.targetId; 11 | return; 12 | } 13 | 14 | const search = PathFinder.search( 15 | this.pos, { 16 | pos: structure.pos, 17 | range: 1, 18 | }, { 19 | maxRooms: 1, 20 | }, 21 | ); 22 | 23 | if (config.visualizer.enabled && config.visualizer.showPathSearches) { 24 | visualizer.showSearch(search); 25 | } 26 | 27 | const pos = search.path[0]; 28 | let returnCode = this.move(this.pos.getDirectionTo(pos)); 29 | 30 | if (returnCode === ERR_NO_PATH) { 31 | this.moveRandom(); 32 | // delete this.memory.routing.targetId; 33 | return true; 34 | } 35 | if (returnCode !== OK && returnCode !== ERR_TIRED) { 36 | // this.log('move returnCode: ' + returnCode); 37 | } 38 | 39 | returnCode = this.dismantle(structure); 40 | if (returnCode === OK) { 41 | this.setNextSpawn(); 42 | this.spawnCarry(); 43 | } 44 | }; 45 | 46 | Creep.prototype.cleanController = function() { 47 | const search = PathFinder.search( 48 | this.pos, { 49 | pos: this.room.controller.pos, 50 | range: 1, 51 | }, { 52 | maxRooms: 1, 53 | }, 54 | ); 55 | if (config.visualizer.enabled && config.visualizer.showPathSearches) { 56 | visualizer.showSearch(search); 57 | } 58 | for (const pos of search.path) { 59 | const posObject = new RoomPosition(pos.x, pos.y, this.room.name); 60 | const structures = posObject.findInRangeStructureToDestroy(1); 61 | 62 | if (structures.length > 0) { 63 | this.memory.routing.targetId = structures[0].id; 64 | this.memory.routing.reached = false; 65 | // this.log('found on way to controller to dismantle: ' + structures[0].pos); 66 | this.moveTo(structures[0].pos); 67 | return true; 68 | } 69 | } 70 | return false; 71 | }; 72 | 73 | Creep.prototype.cleanExits = function() { 74 | const exitDirs = [FIND_EXIT_TOP, 75 | FIND_EXIT_RIGHT, 76 | FIND_EXIT_BOTTOM, 77 | FIND_EXIT_LEFT, 78 | ]; 79 | for (const exitDir of exitDirs) { 80 | const exits = this.room.find(exitDir); 81 | if (exits.length === 0) { 82 | continue; 83 | } 84 | const exit = exits[Math.floor(exits.length / 2)]; 85 | const path = this.pos.findPathTo(exit); 86 | if (path.length === 0) { 87 | continue; 88 | } 89 | const posLast = path[path.length - 1]; 90 | if (!exit.isEqualTo(posLast.x, posLast.y)) { 91 | const pos = new RoomPosition(posLast.x, posLast.y, this.room.name); 92 | const structure = pos.findClosestByRangeStructureToDestroy(); 93 | 94 | if (structure) { 95 | this.memory.routing.targetId = structure.id; 96 | this.log('new memory: ' + structure.id); 97 | return true; 98 | } 99 | } 100 | } 101 | return false; 102 | }; 103 | 104 | Creep.prototype.cleanSetTargetId = function() { 105 | if (!this.room.isMy()) { 106 | // this.log('no targetId'); 107 | if (this.cleanController()) { 108 | // this.log('clean controller'); 109 | return true; 110 | } 111 | if (this.cleanExits()) { 112 | // this.log('clean exits'); 113 | return true; 114 | } 115 | let structure = this.pos.findClosestByRangeStructureToDestroy(); 116 | if (structure !== null) { 117 | const structures = structure.pos.lookFor('structure'); 118 | 119 | if (structures.length > 0) { 120 | for (const structureLook of structures) { 121 | if (structure.structureType === STRUCTURE_RAMPART) { 122 | structure = structureLook; 123 | break; 124 | } 125 | } 126 | } 127 | 128 | this.log('structure: ' + structure.id); 129 | this.memory.routing.targetId = structure.id; 130 | // TODO use the proper pathing logic, just a workaround to fix for now 131 | this.moveTo(structure.pos); 132 | return true; 133 | } 134 | } 135 | this.memory.targetReached = true; 136 | this.memory.killed = true; 137 | this.log('Nothing found, suicide'); 138 | this.suicide(); 139 | // return Creep.recycleCreep(this); 140 | }; 141 | -------------------------------------------------------------------------------- /src/prototype_room_defense.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Room.prototype.findAttackCreeps = function(object) { 4 | if (object.owner.username === 'Source Keeper') { 5 | return false; 6 | } 7 | 8 | for (const item of Object.keys(object.body)) { 9 | const part = object.body[item]; 10 | if (part.energy === 0) { 11 | continue; 12 | } 13 | if (part.type === 'attack') { 14 | return true; 15 | } 16 | if (part.type === 'ranged_attack') { 17 | return true; 18 | } 19 | if (part.type === 'heal') { 20 | return true; 21 | } 22 | if (part.type === 'work') { 23 | return true; 24 | } 25 | if (part.type === 'claim') { 26 | return true; 27 | } 28 | } 29 | return false; 30 | }; 31 | 32 | Room.prototype.handleNukeAttack = function() { 33 | if (!this.executeEveryTicks(config.room.handleNukeAttackInterval)) { 34 | return false; 35 | } 36 | 37 | const nukes = this.findNukes(); 38 | if (nukes.length === 0) { 39 | return false; 40 | } 41 | 42 | const sorted = _.sortBy(nukes, (object) => { 43 | return object.timeToLand; 44 | }); 45 | if (sorted[0].timeToLand < 100) { 46 | this.controller.activateSafeMode(); 47 | } 48 | 49 | const isRampart = function(object) { 50 | return object.structureType === STRUCTURE_RAMPART; 51 | }; 52 | 53 | for (const nuke of nukes) { 54 | const structures = nuke.pos.findInRangeBuildings(4); 55 | 56 | this.log('Nuke attack !!!!!'); 57 | for (const structure of structures) { 58 | const lookConstructionSites = structure.pos.lookFor(LOOK_CONSTRUCTION_SITES); 59 | if (lookConstructionSites.length > 0) { 60 | continue; 61 | } 62 | const lookStructures = structure.pos.lookFor(LOOK_STRUCTURES); 63 | const lookRampart = _.findIndex(lookStructures, isRampart); 64 | if (lookRampart > -1) { 65 | continue; 66 | } 67 | structure.pos.createConstructionSite(STRUCTURE_RAMPART); 68 | } 69 | } 70 | 71 | return true; 72 | }; 73 | 74 | Room.prototype.handleTowerWithEnemies = function(hostileCreeps, towers) { 75 | let tower; 76 | const hostileOffset = {}; 77 | const sortHostiles = function(object) { 78 | return tower.pos.getRangeTo(object) + (hostileOffset[object.id] || 0); 79 | }; 80 | 81 | const towersAttacking = _.sortBy(towers, (object) => { 82 | const hostile = object.pos.findClosestByRange(FIND_HOSTILE_CREEPS); 83 | return object.pos.getRangeTo(hostile); 84 | }); 85 | 86 | for (tower of towersAttacking) { 87 | const hostilesSorted = _.sortBy(hostileCreeps, sortHostiles); 88 | tower.attack(hostilesSorted[0]); 89 | hostileOffset[hostilesSorted[0].id] = 100; 90 | } 91 | return true; 92 | }; 93 | 94 | /** 95 | * letTowersRepairStructures 96 | * 97 | * @param {object} room 98 | * @param {array} towers 99 | */ 100 | function letTowersRepairStructures(room, towers) { 101 | for (const tower of towers) { 102 | if (tower.energy === 0) { 103 | continue; 104 | } 105 | if (!room.executeEveryTicks(10)) { 106 | if (tower.energy < tower.energyCapacity / 2 || room.memory.repair_min > 1000000) { 107 | continue; 108 | } 109 | } 110 | 111 | const lowRampart = tower.pos.findClosestByRangeLowHitRamparts(); 112 | 113 | let repair = lowRampart; 114 | if (lowRampart === null) { 115 | repair = tower.pos.findClosestByRangeLowHitStructures(); 116 | } 117 | tower.repair(repair); 118 | } 119 | } 120 | 121 | Room.prototype.handleTower = function() { 122 | const towers = this.findTowers(); 123 | if (towers.length === 0) { 124 | return false; 125 | } 126 | const hostileCreeps = this.findEnemies(); 127 | if (hostileCreeps.length > 0) { 128 | return this.handleTowerWithEnemies(hostileCreeps, towers); 129 | } 130 | 131 | if (config.tower.healMyCreeps) { 132 | const myCreeps = this.findMyCreepsToHeal(); 133 | if (myCreeps.length > 0) { 134 | for (const tower of towers) { 135 | tower.heal(myCreeps[0]); 136 | } 137 | return true; 138 | } 139 | } 140 | 141 | if (this.controller.level < 4) { 142 | return false; 143 | } 144 | 145 | if (!config.tower.repairStructures) { 146 | return true; 147 | } 148 | 149 | if (!this.memory.repair_min) { 150 | this.memory.repair_min = 0; 151 | } 152 | 153 | letTowersRepairStructures(this, towers); 154 | 155 | return true; 156 | }; 157 | -------------------------------------------------------------------------------- /src/brain_main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {checkPlayers} = require('./diplomacy'); 4 | const {handleQuests} = require('./quests_host'); 5 | const {prepareMemory} = require('./brain_memory'); 6 | const {handleSquadManager} = require('./brain_squadmanager'); 7 | const {detectTrappedScenario} = require('./brain_trapped'); 8 | 9 | global.cpuUsed = 0; 10 | 11 | /** 12 | * leftPadRound 13 | * 14 | * String with fixed width for rounded number 15 | * @param {number} nr 16 | * @param {number} lpad 17 | * @param {number} digest 18 | * @return {string} 19 | */ 20 | function leftPadRound(nr, lpad, digest) { 21 | return nr.toFixed(digest).padStart(lpad + digest + 1); 22 | } 23 | 24 | /** 25 | * executeRooms 26 | * 27 | * Executes all rooms and stores controlled rooms in `Memory.myRooms' 28 | */ 29 | function executeRooms() { 30 | Memory.myRooms = _(Game.rooms).filter((r) => r.execute()).map((r) => r.name).value(); 31 | } 32 | 33 | module.exports.cleanUpDyingCreep = function(name) { 34 | delete Memory.creeps[name]; 35 | }; 36 | 37 | module.exports.initProfiler = function() { 38 | if (config.profiler.enabled) { 39 | try { 40 | global.profiler = require('screeps-profiler'); // eslint-disable-line global-require 41 | for (const role of _.keys(roles)) { 42 | global.profiler.registerObject(roles[role], 'Role_' + role); 43 | } 44 | global.profiler.registerObject(brain, 'Brain'); 45 | global.profiler.enable(); 46 | } catch (e) { 47 | console.log('screeps-profiler not found'); 48 | config.profiler.enabled = false; 49 | } 50 | } 51 | }; 52 | 53 | /** 54 | * visualizeRooms 55 | * 56 | * Renders the visualizer 57 | */ 58 | function visualizeRooms() { 59 | if (config.visualizer.enabled) { 60 | try { 61 | Memory.myRooms.forEach(visualizer.myRoomDataDraw); 62 | } catch (e) { 63 | console.log(`Visualizer Draw Exception exception: ${e} stack ${e.stack}`); 64 | } 65 | try { 66 | visualizer.render(); 67 | } catch (e) { 68 | console.log('Visualizer Render Exception', e, e.stack); 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * updateSkippedRoomsLog 75 | * 76 | * Logs and stores skipped rooms 77 | */ 78 | function updateSkippedRoomsLog() { 79 | Memory.skippedRoomsLog = Memory.skippedRoomsLog || {}; 80 | if (_.size(Memory.skippedRooms) > 0) { 81 | console.log(`${Game.time} cpu.getUsed: ${_.round(Game.cpu.getUsed())} tickLimit: ${Game.cpu.tickLimit} Bucket: ${Game.cpu.bucket} skippedRooms ${Memory.skippedRooms}`); 82 | Memory.skippedRoomsLog[Game.time] = Memory.skippedRooms; 83 | } 84 | if (Game.time % 100 === 0) { 85 | const roomsSkipped = _.sum(_.map(Memory.skippedRoomsLog, _.size)); 86 | const lowExecution = _.size(Memory.skippedRoomsLog) / 100 < config.main.lowExecution; 87 | if (config.debug.cpu && lowExecution) { 88 | Game.notify(`${Game.time} skipped rooms ${roomsSkipped} in ${_.size(Memory.skippedRoomsLog)} ticks of 100 ticks`, 120); 89 | } 90 | Memory.skippedRoomsLog = {}; 91 | } 92 | } 93 | 94 | module.exports.execute = function() { 95 | if (Game.time > 1000 && Game.cpu.bucket < 1.5 * Game.cpu.tickLimit && Game.cpu.bucket < Game.cpu.limit * 10) { 96 | console.log(`${Game.time} Skipping tick CPU Bucket too low. bucket: ${Game.cpu.bucket} tickLimit: ${Game.cpu.tickLimit} limit: ${Game.cpu.limit}`); 97 | return; 98 | } 99 | Memory.time = Game.time; 100 | try { 101 | prepareMemory(); 102 | brain.buyPower(); 103 | brain.handleNextroomer(); 104 | detectTrappedScenario(); 105 | handleSquadManager(); 106 | brain.handleIncomingTransactions(); 107 | handleQuests(); 108 | checkPlayers(); 109 | } catch (e) { 110 | console.log('Brain Exception', e.stack); 111 | } 112 | 113 | brain.stats.addRoot(); 114 | executeRooms(); 115 | visualizeRooms(); 116 | updateSkippedRoomsLog(); 117 | brain.stats.add(['cpu'], { 118 | used: Game.cpu.getUsed(), 119 | }); 120 | 121 | if (global.config.tickSummary.gcl) { 122 | console.log(`${Game.time} GCL ${Game.gcl.level}: ${leftPadRound(Game.gcl.progress / Game.gcl.progressTotal * 100, 3, 5)} % ${Math.round(Game.gcl.progress)}/${Math.round(Game.gcl.progressTotal)}`); 123 | } 124 | if (global.config.tickSummary.bucket) { 125 | console.log(`${Game.time} Bucket: ${Game.cpu.bucket}`); 126 | } 127 | if (global.config.tickSummary.separator) { 128 | console.log(Game.time, '-----------'); 129 | } 130 | }; 131 | -------------------------------------------------------------------------------- /src/prototype_room_controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const resetCounters = function(room) { 4 | if (!room.memory.controllerLevel) { 5 | // After deleting room memory this was an issue 6 | room.memory.controllerLevel = {}; 7 | } 8 | room.memory.controllerLevel.checkPathInterval = 1; 9 | room.memory.controllerLevel.checkWrongStructureInterval = 1; 10 | room.memory.controllerLevel.buildStructuresInterval = 1; 11 | room.memory.controllerLevel.buildBlockersInterval = 1; 12 | }; 13 | 14 | Room.prototype.buildBaseUnseenControllerLevel = function() { 15 | this.memory.controllerLevel = this.memory.controllerLevel || {}; 16 | if (!this.memory.controllerLevel['setup_level_' + this.controller.level]) { 17 | if (Game.cpu.tickLimit > Game.cpu.bucket) { 18 | this.debugLog('baseBuilding', `Skipping room_controller.js execution CPU limit: ${Game.cpu.limit} tickLimit: ${Game.cpu.tickLimit} bucket: ${Game.cpu.bucket}`); 19 | return true; 20 | } 21 | // TODO I don't think we need this here, after change to heap 22 | this.updateCostMatrix(); 23 | resetCounters(this); 24 | 25 | // Clear isActive cache when RCL changes 26 | if (global.isActiveCache && global.isActiveCache[this.name]) { 27 | delete global.isActiveCache[this.name]; 28 | this.debugLog('baseBuilding', `Cleared isActive cache for ${this.name} at RCL ${this.controller.level}`); 29 | } 30 | 31 | this.memory.controllerLevel['setup_level_' + this.controller.level] = Game.time; 32 | } 33 | }; 34 | 35 | const executeTask = function(room, name) { 36 | if (room[name]()) { 37 | room.memory.controllerLevel[name + 'Interval'] = 1; 38 | } else { 39 | room.memory.controllerLevel[name + 'Interval']++; 40 | } 41 | }; 42 | 43 | Room.prototype.buildBaseCheckPath = function() { 44 | if (this.controller.level > 2 && this.executeEveryTicks(this.memory.controllerLevel.checkPathInterval)) { 45 | if (this.checkPath(this)) { 46 | resetCounters(this); 47 | } else { 48 | if (!this.memory.controllerLevel) { 49 | // After deleting room memories this ran into an issue 50 | resetCounters(this); 51 | } 52 | this.memory.controllerLevel.checkPathInterval++; 53 | } 54 | } 55 | }; 56 | 57 | /** 58 | * checkWrongStructures 59 | * 60 | * @param {object} room 61 | */ 62 | function checkWrongStructures(room) { 63 | if (!room.memory.controllerLevel.checkWrongStructureInterval) { 64 | room.memory.controllerLevel.checkWrongStructureInterval = 1; 65 | } 66 | if (room.memory.walls && room.memory.walls.finished && room.executeEveryTicks(room.memory.controllerLevel.checkWrongStructureInterval)) { 67 | if (room.checkWrongStructure(room)) { 68 | resetCounters(room); 69 | } else { 70 | room.memory.controllerLevel.checkWrongStructureInterval++; 71 | } 72 | } 73 | } 74 | 75 | Room.prototype.buildBase = function() { 76 | // version: this.memory.position.version is maybe not the best idea 77 | if (this.data.positions.version !== config.layout.version) { 78 | this.debugLog('baseBuilding', 'New layout version, rebuilding'); 79 | this.memory = {}; 80 | this.data.routing = {}; 81 | this.data.positions = {}; 82 | this.setup(); 83 | } else if (!this.memory.setup || !this.memory.setup.completed) { 84 | this.setup(); 85 | } 86 | if (this.buildBaseUnseenControllerLevel()) { 87 | return; 88 | } 89 | 90 | this.buildBaseCheckPath(); 91 | checkWrongStructures(this); 92 | 93 | if (!this.memory.controllerLevel.buildStructuresInterval) { 94 | this.memory.controllerLevel.buildStructuresInterval = 1; 95 | } 96 | if (this.executeEveryTicks(this.memory.controllerLevel.buildStructuresInterval)) { 97 | executeTask(this, 'buildStructures'); 98 | } 99 | 100 | if (!this.memory.controllerLevel.checkBlockersInterval) { 101 | this.memory.controllerLevel.checkBlockersInterval = 1; 102 | } 103 | if (!this.memory.controllerLevel.buildBlockersInterval) { 104 | this.memory.controllerLevel.buildBlockersInterval = 1; 105 | } 106 | if (this.memory.controllerLevel.buildStructuresInterval > 1) { 107 | if (this.executeEveryTicks(this.memory.controllerLevel.checkBlockersInterval)) { 108 | executeTask(this, 'checkBlockers'); 109 | } 110 | if (this.executeEveryTicks(this.memory.controllerLevel.buildBlockersInterval)) { 111 | if (this.controller.level >= 2) { 112 | executeTask(this, 'buildBlockers'); 113 | } 114 | } 115 | } 116 | }; 117 | 118 | Room.prototype.clearRoom = function() { 119 | const structures = this.findStructures(); 120 | _.each(structures, (s) => s.destroy()); 121 | 122 | const constructionSites = this.findConstructionSites(); 123 | _.each(constructionSites, (cs) => cs.remove()); 124 | 125 | const creeps = this.findMyCreeps(); 126 | _.each(creeps, (cs) => cs.suicide()); 127 | }; 128 | -------------------------------------------------------------------------------- /src/quests_host.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const {debugLog} = require('./logging'); 4 | const {splitRoomName} = require('./prototype_room_utils'); 5 | 6 | /** 7 | * handleQuests 8 | * 9 | * Checks the current quests, send a quester in case, or ends the quest 10 | */ 11 | function handleQuests() { 12 | Memory.quests = Memory.quests || {}; 13 | for (const id of Object.keys(Memory.quests)) { 14 | const quest = Memory.quests[id]; 15 | 16 | if (!quest.checked && quest.check < Game.time) { 17 | debugLog('quests', `Spawn quester: ${JSON.stringify(quest)}`); 18 | Memory.quests[id].checked = true; 19 | Game.rooms[quest.origin].checkRoleToSpawn('quester', 1, undefined, quest.room, quest.id, quest.origin); 20 | } 21 | 22 | if (quest.end < Game.time) { 23 | debugLog('quests', `handleQuests quest ended: ${JSON.stringify(quest)}`); 24 | delete Memory.quests[id]; 25 | } 26 | } 27 | } 28 | 29 | module.exports.handleQuests = handleQuests; 30 | 31 | 32 | /** 33 | * checkQuestForAcceptance 34 | * 35 | * @param {object} transaction 36 | * @return {bool} 37 | */ 38 | function checkQuestForAcceptance(transaction) { 39 | Memory.quests = Memory.quests || {}; 40 | const data = getQuestFromTransactionDescription(transaction.description); 41 | if (!data) { 42 | return false; 43 | } 44 | if (Memory.quests[data.id]) { 45 | console.log(`Quest already ongoing ${JSON.stringify(data)}`); 46 | return; 47 | } 48 | const quest = getQuest(transaction, data); 49 | console.log(`Found quest acceptance on transaction ${JSON.stringify(quest)}`); 50 | 51 | Memory.quests[data.id] = quest; 52 | 53 | const response = { 54 | type: 'quest', 55 | id: quest.id, 56 | room: quest.room, 57 | quest: quest.quest, 58 | end: quest.end, 59 | }; 60 | const room = Game.rooms[transaction.to]; 61 | console.log(`Send accept quest: ${JSON.stringify(response)}`); 62 | const terminalResponse = room.terminal.send(RESOURCE_ENERGY, 100, transaction.from, JSON.stringify(response)); 63 | console.log(`terminalResponse ${JSON.stringify(terminalResponse)}`); 64 | // TODO find reserver in room and remove quest hint 65 | } 66 | module.exports.checkQuestForAcceptance = checkQuestForAcceptance; 67 | 68 | /** 69 | * getQuestRoom 70 | * 71 | * Finds a room close by `roomName` 72 | * 73 | * @param {string} roomName 74 | */ 75 | function getQuestRoom(roomName) { 76 | const parts = splitRoomName(roomName); 77 | debugLog('quests', `getQuestRoom parts: ${JSON.stringify(parts)}`); 78 | for (let x=-3; x<=3; x++) { 79 | const xvalue = x + parseInt(parts[2], 10); 80 | const roomName = `${parts[1]}${xvalue}${parts[3]}${parts[4]}`; 81 | debugLog('quests', `getQuestRoom roomName: ${roomName}`); 82 | } 83 | } 84 | 85 | /** 86 | * getQuestBuildConstructionSite 87 | * 88 | * @param {object} data 89 | * @param {string} roomName 90 | * @return {object} 91 | */ 92 | function getQuestBuildConstructionSite(data, roomName) { 93 | debugLog('quests', `getQuestBuildConstructionSite(${JSON.stringify(data)})`); 94 | const quest = {}; 95 | quest.room = getQuestRoom(roomName); // TODO a room needs to be selected where to build the construction sites 96 | quest.quest = 'buildcs'; 97 | quest.end = Game.time + 15000; 98 | quest.check = Game.time + 0; 99 | return quest; 100 | } 101 | 102 | /** 103 | * getQuest 104 | * 105 | * @param {object} transaction 106 | * @param {object} data 107 | * @return {object} 108 | */ 109 | function getQuest(transaction, data) { 110 | const info = {}; 111 | info.id = data.id; 112 | info.player = { 113 | name: transaction.sender.username, 114 | room: transaction.from, 115 | }; 116 | info.origin = transaction.to; 117 | 118 | const quest = getQuestBuildConstructionSite(data, transaction.from); 119 | 120 | info.room = quest.room; 121 | info.quest = quest.quest; 122 | info.end = quest.end; 123 | info.check = quest.check; 124 | return info; 125 | } 126 | 127 | /** 128 | * getQuestFromTransactionDescription 129 | * 130 | * @param {object} description 131 | * @return {bool} 132 | */ 133 | function getQuestFromTransactionDescription(description) { 134 | let data; 135 | try { 136 | data = JSON.parse(description); 137 | } catch (e) { 138 | debugLog('quests', 'Quest transaction: Can not parse'); 139 | return false; 140 | } 141 | if (data === null) { 142 | debugLog('quests', 'Quest transaction: No type'); 143 | return false; 144 | } 145 | for (const key of ['type', 'action', 'id']) { 146 | if (!data[key]) { 147 | debugLog('quests', `Incoming transaction no Quest: No ${key}`); 148 | return false; 149 | } 150 | } 151 | if (data.type !== 'quest') { 152 | debugLog('quests', 'Quest transaction: Type not quest'); 153 | return false; 154 | } 155 | if (data.action !== 'apply') { 156 | debugLog('quests', 'Quest transaction: action not apply'); 157 | return false; 158 | } 159 | return data; 160 | } 161 | 162 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./require'); 4 | require('./prototype_creep_startup_tasks'); 5 | require('./prototype_creep_move'); 6 | require('./prototype_roomPosition'); 7 | require('./prototype_room_init'); 8 | require('./prototype_room_costmatrix'); 9 | require('./visualizer'); 10 | require('./screepsplus'); 11 | 12 | const {initProfiler, execute} = require('./brain_main'); 13 | const {cpuLimit} = require('./brain_stats'); 14 | const {generatePixel} = require('./pixel'); 15 | 16 | global.tickLimit = cpuLimit(); 17 | global.load = Math.round(Game.cpu.getUsed()); 18 | 19 | // Init heap data 20 | global.data = global.data || {}; 21 | global.data.creeps = global.data.creeps || {}; 22 | global.data.rooms = global.data.rooms || {}; 23 | global.data.stats = global.data.stats || { 24 | cpuIdle: 0, 25 | memoryFree: 0, 26 | heapFree: 0, 27 | }; 28 | 29 | console.log(`${Game.time} Script reload - Load: ${global.load} tickLimit: ${Game.cpu.tickLimit} limit: ${Game.cpu.limit} Bucket: ${Game.cpu.bucket}`); 30 | 31 | brain.stats.init(); 32 | initProfiler(); 33 | 34 | /** 35 | * Get cached isActive() result for a structure to reduce CPU usage 36 | * Cache persists across ticks and is cleared only when RCL changes 37 | * 38 | * @param {Structure} structure - Any structure object 39 | * @return {boolean} Whether the structure is active 40 | */ 41 | global.getCachedIsActive = function(structure) { 42 | if (!global.isActiveCache) { 43 | global.isActiveCache = {}; 44 | } 45 | const roomName = structure.room.name; 46 | if (!global.isActiveCache[roomName]) { 47 | global.isActiveCache[roomName] = {}; 48 | } 49 | if (global.isActiveCache[roomName][structure.id] === undefined) { 50 | global.isActiveCache[roomName][structure.id] = structure.isActive(); 51 | } 52 | return global.isActiveCache[roomName][structure.id]; 53 | }; 54 | 55 | /** 56 | * Main game loop - executed every tick 57 | */ 58 | module.exports.loop = function() { 59 | // Initialize global cache for isActive() calls (persists across ticks) 60 | global.isActiveCache = global.isActiveCache || {}; 61 | 62 | try { 63 | runMainLogic(); 64 | } catch (error) { 65 | console.log(`ERROR: Main loop crashed at tick ${Game.time}: ${error.message}`); 66 | console.log(`Stack: ${error.stack}`); 67 | // Continue to run stats/pixel generation even if main logic fails 68 | } 69 | 70 | try { 71 | runPostTickOperations(); 72 | } catch (error) { 73 | console.log(`ERROR: Post-tick operations failed at tick ${Game.time}: ${error.message}`); 74 | } 75 | }; 76 | 77 | /** 78 | * Run the main bot logic 79 | */ 80 | function runMainLogic() { 81 | if (config.main.enabled) { 82 | if (config.profiler.enabled) { 83 | global.profiler.wrap(() => { 84 | execute(); 85 | }); 86 | } else { 87 | execute(); 88 | } 89 | } 90 | } 91 | 92 | /** 93 | * Run post-tick operations (stats, pixels) 94 | */ 95 | function runPostTickOperations() { 96 | generatePixel(); 97 | brain.stats.updateCpuStats(); 98 | 99 | if (config.nextRoom.resourceStats) { 100 | updateResourceStats(); 101 | } 102 | } 103 | 104 | /** 105 | * Update resource usage statistics with exponential smoothing 106 | */ 107 | function updateResourceStats() { 108 | const statsDivider = config.nextRoom.resourceStatsDivider; 109 | const cpuLimit = Game.cpu.limit; 110 | const currentCPUUsed = Game.cpu.getUsed(); 111 | const currentIdleCPU = cpuLimit - currentCPUUsed; 112 | 113 | // CPU stats 114 | global.data.stats.cpuIdle = ((statsDivider - 1) * global.data.stats.cpuIdle + currentIdleCPU) / statsDivider; 115 | global.data.stats.cpuUsed = global.data.stats.cpuUsed || cpuLimit; 116 | global.data.stats.cpuUsed = ((statsDivider - 1) * global.data.stats.cpuUsed + currentCPUUsed) / statsDivider; 117 | 118 | // Heap stats 119 | const heapStatistics = Game.cpu.getHeapStatistics(); 120 | const heapLimit = heapStatistics.heap_size_limit; 121 | const currentHeapUsed = heapStatistics.used_heap_size + heapStatistics.externally_allocated_size; 122 | const currentHeapFree = heapLimit - currentHeapUsed; 123 | global.data.stats.heapFree = ((statsDivider - 1) * global.data.stats.heapFree + currentHeapFree) / statsDivider; 124 | global.data.stats.heapUsed = global.data.stats.heapUsed || heapLimit; 125 | global.data.stats.heapUsed = ((statsDivider - 1) * global.data.stats.heapUsed + currentHeapUsed) / statsDivider; 126 | 127 | // Memory stats 128 | const MEMORY_LIMIT = 2000000; // 2MB Screeps memory limit 129 | const currentMemoryUsed = RawMemory.get().length; 130 | const currentMemoryFree = MEMORY_LIMIT - currentMemoryUsed; 131 | global.data.stats.memoryFree = ((statsDivider - 1) * global.data.stats.memoryFree + currentMemoryFree) / statsDivider; 132 | global.data.stats.memoryUsed = global.data.stats.memoryUsed || MEMORY_LIMIT; 133 | global.data.stats.memoryUsed = ((statsDivider - 1) * global.data.stats.memoryUsed + currentMemoryUsed) / statsDivider; 134 | } 135 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "screeps-bot-tooangel", 3 | "version": "1.5.2", 4 | "description": "TooAngel NPC / bot / source code for screeps", 5 | "main": "src/main.js", 6 | "screeps_bot": true, 7 | "devDependencies": { 8 | "chai": "4.3.7", 9 | "eslint": "8.35.0", 10 | "eslint-config-google": "0.14.0", 11 | "eslint-plugin-screeps": "2.1.0", 12 | "grunt": "1.6.1", 13 | "grunt-contrib-clean": "2.0.1", 14 | "grunt-contrib-copy": "1.0.0", 15 | "grunt-contrib-uglify": "5.2.2", 16 | "grunt-exec": "3.0.0", 17 | "grunt-mocha-test": "0.13.3", 18 | "grunt-screeps": "1.5.0", 19 | "grunt-sync": "0.8.2", 20 | "mocha": "10.2.0", 21 | "request": "2.88.2", 22 | "request-promise-native": "1.0.9", 23 | "rimraf": "3.0.2", 24 | "screeps": "4.2.14", 25 | "screeps-api": "1.16.1", 26 | "screeps-profiler": "2.0.1", 27 | "screepsmod-admin-utils": "1.29.0", 28 | "screepsmod-auth": "2.7.1", 29 | "screepsmod-mongo": "2.10.4", 30 | "uglify-js-harmony": "2.7.7" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git://github.com/TooAngel/screeps.git" 35 | }, 36 | "author": "Tobias Wilken", 37 | "license": "AGPL-3.0", 38 | "bugs": { 39 | "url": "https://github.com/TooAngel/screeps/issues" 40 | }, 41 | "scripts": { 42 | "test": "eslint . && grunt test", 43 | "test-no-server": "grunt test_no_server", 44 | "deploy": "grunt deploy", 45 | "lint": "eslint .", 46 | "run-test": "git checkout test-server/db.json && node utils/test_on_server.js", 47 | "setupTestServer": "docker compose -f docker-compose-setup.yml up", 48 | "followLogs": "node utils/followLogs localhost true", 49 | "deployLocal": "screeps-api upload src/*", 50 | "respawner": "node utils/respawner" 51 | }, 52 | "eslintConfig": { 53 | "extends": [ 54 | "eslint:recommended", 55 | "google" 56 | ], 57 | "parserOptions": { 58 | "ecmaVersion": 2022 59 | }, 60 | "env": { 61 | "mocha": true, 62 | "screeps/screeps": true, 63 | "es6": true, 64 | "node": true 65 | }, 66 | "plugins": [ 67 | "screeps" 68 | ], 69 | "globals": { 70 | "config": "writable", 71 | "brain": "writable", 72 | "roles": "writable", 73 | "friends": "writable", 74 | "cache": "writable", 75 | "visualizer": "writable", 76 | "PIXEL_CPU_COST": "writable", 77 | "describe": "writable", 78 | "it": "writable", 79 | "getCachedIsActive": "readonly" 80 | }, 81 | "ignorePatterns": [ 82 | "dist/", 83 | "**/*{.,-}min.js", 84 | "tmp-test-server/", 85 | "tmp-test-server-database/" 86 | ], 87 | "rules": { 88 | "complexity": [ 89 | "error", 90 | 13 91 | ], 92 | "max-statements": [ 93 | "error", 94 | 30 95 | ], 96 | "accessor-pairs": "error", 97 | "no-alert": "error", 98 | "no-div-regex": "error", 99 | "no-eq-null": "error", 100 | "no-eval": "error", 101 | "no-implied-eval": "error", 102 | "no-iterator": "error", 103 | "no-lone-blocks": "error", 104 | "no-loop-func": "error", 105 | "no-native-reassign": "error", 106 | "no-new-func": "error", 107 | "no-new": "error", 108 | "no-octal-escape": "error", 109 | "no-proto": "error", 110 | "no-return-assign": "error", 111 | "no-script-url": "error", 112 | "no-self-compare": "error", 113 | "no-trailing-spaces": "error", 114 | "no-unused-expressions": "error", 115 | "no-useless-call": "error", 116 | "no-useless-concat": "error", 117 | "no-void": "error", 118 | "radix": "error", 119 | "wrap-iife": "error", 120 | "no-catch-shadow": "error", 121 | "no-label-var": "error", 122 | "no-shadow-restricted-names": "error", 123 | "no-undef-init": "error", 124 | "callback-return": "error", 125 | "global-require": "error", 126 | "handle-callback-err": "error", 127 | "no-path-concat": "error", 128 | "no-process-exit": "error", 129 | "max-len": [ 130 | "error", 131 | 240 132 | ], 133 | "max-statements-per-line": "error", 134 | "prefer-arrow-callback": "error", 135 | "no-undef": "warn", 136 | "no-cond-assign": "error", 137 | "no-console": "off", 138 | "no-negated-in-lhs": "error", 139 | "prefer-const": "error", 140 | "eqeqeq": [ 141 | "error", 142 | "always" 143 | ], 144 | "space-before-function-paren": [ 145 | "error", 146 | "never" 147 | ], 148 | "no-unused-vars": [ 149 | "error", 150 | { 151 | "varsIgnorePattern": "should|expect|assert" 152 | } 153 | ], 154 | "indent": [ 155 | "error", 156 | 2 157 | ], 158 | "require-jsdoc": [ 159 | "error", 160 | { 161 | "require": { 162 | "FunctionDeclaration": true, 163 | "MethodDefinition": true, 164 | "ClassDeclaration": true 165 | } 166 | } 167 | ] 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/role_universal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * universal makes sure that extensions are filled 5 | * 6 | * Before storage or certain store threshold: 7 | * - get dropped energy or from source 8 | * - fill extensions 9 | * - build constructionSites 10 | * - upgrade Controller 11 | * 12 | * Proper storage store level: 13 | * - Move along the universal path 14 | * - pathPos === 0 get energy from storage 15 | * - transfer energy to extensions in range 16 | */ 17 | 18 | roles.universal = {}; 19 | 20 | roles.universal.settings = { 21 | param: ['controller.level', 'energyAvailable'], 22 | layoutString: 'MWC', 23 | amount: { 24 | 1: [2, 1, 1], 25 | 2: { 26 | 0: [2, 1, 1], 27 | 550: [4, 3, 1], 28 | 750: [2, 1, 1], 29 | }, 30 | }, 31 | maxLayoutAmount: 6, 32 | }; 33 | 34 | /** 35 | * @param {object} room - the room to spawn in 36 | * @return {{maxLayoutAmount: number}|{amount: number[], maxLayoutAmount: number, layoutString: string, prefixString: string}} 37 | */ 38 | roles.universal.updateSettings = function(room) { 39 | if (!room.isStruggling() && room.energyAvailable >= 350) { 40 | // LayoutCost minimum: prefix 250 + layout 100 -> 350 41 | return { 42 | prefixString: 'WMC', 43 | layoutString: 'MC', 44 | amount: [1, 2], 45 | maxLayoutAmount: 12, 46 | }; 47 | } else if (room.storage && !room.storage.my) { 48 | return { 49 | maxLayoutAmount: 999, 50 | }; 51 | } 52 | }; 53 | 54 | roles.universal.buildRoad = true; 55 | roles.universal.boostActions = ['capacity']; 56 | 57 | const universalBeforeStorage = function(creep) { 58 | if (creep.room.isStruggling()) { 59 | creep.universalBeforeStorage(); 60 | creep.memory.routing.reached = false; 61 | return true; 62 | } 63 | return false; 64 | }; 65 | 66 | /** 67 | * pickupResourcesInRange 68 | * 69 | * @param {object} creep 70 | */ 71 | function pickupResourcesInRange(creep) { 72 | const resources = creep.pos.findInRange(FIND_DROPPED_RESOURCES, 1); 73 | if (resources.length > 0) { 74 | creep.pickup(resources[0]); 75 | } 76 | } 77 | 78 | /** 79 | * handlePathEnd 80 | * 81 | * @param {object} creep 82 | */ 83 | function handlePathEnd(creep) { 84 | if (creep.room.memory.position.pathEndLevel) { 85 | if (creep.memory.routing.pathPos >= creep.room.memory.position.pathEndLevel[creep.room.controller.level]) { 86 | creep.memory.move_forward_direction = false; 87 | creep.memory.routing.reverse = true; 88 | delete creep.memory.routing.reached; 89 | } 90 | } 91 | } 92 | 93 | /** 94 | * handlePathStart 95 | * 96 | * @param {object} creep 97 | * @param {boolean} reverse 98 | * @return {boolean} 99 | */ 100 | function handlePathStart(creep, reverse) { 101 | if (creep.memory.routing.pathPos === 0) { 102 | for (const resource in creep.carry) { 103 | if (resource === RESOURCE_ENERGY) { 104 | continue; 105 | } 106 | creep.transfer(creep.room.storage, resource); 107 | } 108 | 109 | const returnCode = creep.withdraw(creep.room.storage, RESOURCE_ENERGY); 110 | if (returnCode === OK || returnCode === ERR_FULL) { 111 | creep.memory.move_forward_direction = true; 112 | reverse = false; 113 | creep.memory.routing.reverse = false; 114 | if (returnCode === OK) { 115 | return true; 116 | } 117 | } 118 | } 119 | creep.memory.routing.reverse = reverse || !creep.memory.move_forward_direction; 120 | } 121 | 122 | roles.universal.preMove = function(creep, directions) { 123 | creep.creepLog(`preMove`); 124 | if (universalBeforeStorage(creep)) { 125 | return true; 126 | } 127 | 128 | pickupResourcesInRange(creep); 129 | 130 | if (typeof(creep.memory.move_forward_direction) === 'undefined') { 131 | creep.memory.move_forward_direction = true; 132 | } 133 | 134 | // changed control flow: first transferToStructures then universalBeforeStorage 135 | let reverse = creep.carry.energy === 0; 136 | 137 | creep.setNextSpawn(); 138 | const configuredAmount = creep.room.getUniversalAmount(); 139 | creep.spawnReplacement(configuredAmount); 140 | 141 | const transferred = creep.transferToStructures(); 142 | if (transferred) { 143 | if (transferred.transferred >= _.sum(creep.carry)) { 144 | reverse = true; 145 | } else { 146 | if (transferred.moreStructures) { 147 | return true; 148 | } 149 | } 150 | } 151 | 152 | creep.memory.routing.targetId = 'universal'; 153 | 154 | if (handlePathStart(creep, reverse)) { 155 | return true; 156 | } 157 | 158 | if (directions && creep.memory.routing.reverse) { 159 | directions.direction = directions.backwardDirection; 160 | } 161 | 162 | handlePathEnd(creep); 163 | }; 164 | 165 | roles.universal.action = function(creep) { 166 | creep.creepLog(`action`); 167 | if (!creep.memory.routing.targetId) { 168 | creep.memory.routing.targetId = 'universal'; 169 | } 170 | 171 | if (universalBeforeStorage(creep)) { 172 | return true; 173 | } 174 | 175 | creep.memory.move_forward_direction = false; 176 | creep.memory.routing.reverse = true; 177 | delete creep.memory.routing.reached; 178 | return true; 179 | }; 180 | -------------------------------------------------------------------------------- /src/brain_squadmanager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {debugLog} = require('./logging'); 4 | 5 | module.exports.handleSquadManager = function() { 6 | if (Object.keys(Memory.squads).length > 0) { 7 | debugLog('brain', `brain.handleSquadManager squads: ${Object.keys(Memory.squads).length}`); 8 | } 9 | for (const squadIndex of Object.keys(Memory.squads)) { 10 | const squad = Memory.squads[squadIndex]; 11 | if (!squad.siege || Object.keys(squad.siege).length === 0) { 12 | continue; 13 | } 14 | if (squad.action === 'move') { 15 | let allReady = true; 16 | 17 | // Check if all siege creeps are waiting 18 | for (const siegeId of Object.keys(squad.siege)) { 19 | const siege = squad.siege[siegeId]; 20 | if (!siege.waiting) { 21 | allReady = false; 22 | break; 23 | } 24 | } 25 | 26 | // Check if all heal creeps are waiting 27 | if (allReady) { 28 | for (const healId of Object.keys(squad.heal)) { 29 | const heal = squad.heal[healId]; 30 | if (!heal.waiting) { 31 | allReady = false; 32 | break; 33 | } 34 | } 35 | } 36 | 37 | // Transition to attack if all creeps are ready 38 | if (allReady) { 39 | squad.action = 'attack'; 40 | } 41 | } 42 | } 43 | }; 44 | 45 | /** 46 | * TODO atm addToQueue is only for squad creation usable 47 | * TODO check for queue.length split queue creation to all unused (or less than closestSpawn.memory.queue.length) spawns in range (e.g. under 6 rooms, move '6' to config or room.memory) 48 | * 49 | * @param {Array} spawns Array of Objects like {creeps: 1, role: 'squadsiege'} 50 | * @param {String} roomNameFrom pushing to Game.rooms[roomNameFrom].memory.queue 51 | * @param {String} roomNameTarget routing target 52 | * @param {String} squadName 53 | * @param {Number} [queueLimit] don't push if queueLimit is reached 54 | * @return {void} 55 | */ 56 | function addToQueue(spawns, roomNameFrom, roomNameTarget, squadName, queueLimit) { 57 | for (const spawn of spawns) { 58 | _.times(spawn.creeps, () => { 59 | // Check queue limit if specified 60 | if (queueLimit && Memory.rooms[roomNameFrom].queue.length >= queueLimit) { 61 | return; 62 | } 63 | 64 | Memory.rooms[roomNameFrom].queue.push({ 65 | role: spawn.role, 66 | routing: { 67 | targetRoom: roomNameTarget, 68 | }, 69 | squad: squadName, 70 | }); 71 | }); 72 | } 73 | } 74 | 75 | /** 76 | * createSquad - Internal helper to create squad with consistent structure 77 | * 78 | * @param {String} type - Squad type prefix for naming (e.g., 'siege', 'melee') 79 | * @param {String} roomNameFrom - Room to spawn creeps from 80 | * @param {String} roomNameAttack - Target room to attack 81 | * @param {Array} spawns - Array of spawn definitions {creeps: number, role: string} 82 | * @param {Object} squadTypes - Object with squad role types as keys (e.g., {siege: {}, heal: {}}) 83 | * @return {String} Squad name 84 | */ 85 | function createSquad(type, roomNameFrom, roomNameAttack, spawns, squadTypes) { 86 | const name = `${type}-${roomNameFrom}-${roomNameAttack}-${Game.time}`; 87 | const route = Game.map.findRoute(roomNameFrom, roomNameAttack); 88 | const target = route.length > 1 ? route[route.length - 2].room : roomNameFrom; 89 | 90 | Memory.squads = Memory.squads || {}; 91 | addToQueue(spawns, roomNameFrom, roomNameAttack, name); 92 | 93 | Memory.squads[name] = { 94 | born: Game.time, 95 | target: roomNameAttack, 96 | from: roomNameFrom, 97 | route: route, 98 | action: 'move', 99 | moveTarget: target, 100 | ...squadTypes, 101 | }; 102 | 103 | return name; 104 | } 105 | 106 | /** 107 | * startSquad used to attack player.rooms 108 | * 109 | * @param {String} roomNameFrom 110 | * @param {String} roomNameAttack 111 | * @return {String} Squad name 112 | */ 113 | function startSquad(roomNameFrom, roomNameAttack) { 114 | const siegeSpawns = [{ 115 | creeps: 1, 116 | role: 'squadsiege', 117 | }, { 118 | creeps: 3, 119 | role: 'squadheal', 120 | }]; 121 | 122 | return createSquad('siege', roomNameFrom, roomNameAttack, siegeSpawns, { 123 | siege: {}, 124 | heal: {}, 125 | }); 126 | } 127 | module.exports.startSquad = startSquad; 128 | 129 | /** 130 | * startMeleeSquad use to clean rooms from invaders and players 131 | * 132 | * @param {String} roomNameFrom 133 | * @param {String} roomNameAttack 134 | * @param {Array} [spawns] 135 | * @return {String} Squad name 136 | */ 137 | function startMeleeSquad(roomNameFrom, roomNameAttack, spawns) { 138 | // TODO check for queue length 139 | const defaultSpawns = [{ 140 | creeps: 1, 141 | role: 'autoattackmelee', 142 | }, { 143 | creeps: 1, 144 | role: 'squadheal', 145 | }, { 146 | creeps: 2, 147 | role: 'autoattackmelee', 148 | }, { 149 | creeps: 2, 150 | role: 'squadheal', 151 | }]; 152 | 153 | return createSquad('melee', roomNameFrom, roomNameAttack, spawns || defaultSpawns, { 154 | autoattackmelee: {}, 155 | heal: {}, 156 | }); 157 | } 158 | module.exports.startMeleeSquad = startMeleeSquad; 159 | -------------------------------------------------------------------------------- /src/prototype_room_find.js: -------------------------------------------------------------------------------- 1 | const {isFriend} = require('./diplomacy'); 2 | 3 | 4 | Room.prototype.findStructuresWithUsableEnergy = function() { 5 | return this.find(FIND_MY_STRUCTURES, {filter: (object) => { 6 | if (!object.store) { 7 | return false; 8 | } 9 | if (!object.store.energy) { 10 | return false; 11 | } 12 | if (object.structureType === STRUCTURE_SPAWN) { 13 | return false; 14 | } 15 | return true; 16 | }}); 17 | }; 18 | 19 | Room.prototype.findOtherPlayerCreeps = function() { 20 | return this.find(FIND_HOSTILE_CREEPS, {filter: (object) => !global.config.maliciousNpcUsernames.includes(object.owner.username)}); 21 | }; 22 | 23 | Room.prototype.findObservers = function() { 24 | return this.find(FIND_MY_STRUCTURES, {filter: {structureType: STRUCTURE_OBSERVER}}); 25 | }; 26 | 27 | Room.prototype.findMySpawns = function() { 28 | return this.find(FIND_MY_SPAWNS); 29 | }; 30 | 31 | Room.prototype.findMyCreeps = function() { 32 | return this.find(FIND_MY_CREEPS); 33 | }; 34 | 35 | Room.prototype.findStructures = function() { 36 | return this.find(FIND_STRUCTURES); 37 | }; 38 | 39 | Room.prototype.findMyStructures = function() { 40 | return this.find(FIND_MY_STRUCTURES); 41 | }; 42 | 43 | Room.prototype.findConstructionSites = function() { 44 | return this.find(FIND_CONSTRUCTION_SITES); 45 | }; 46 | 47 | Room.prototype.findConstructionSitesStructures = function() { 48 | return this.find(FIND_CONSTRUCTION_SITES, {filter: (object) => ![STRUCTURE_ROAD, STRUCTURE_WALL, STRUCTURE_RAMPART].includes(object.structureType)}); 49 | }; 50 | 51 | Room.prototype.findConstructionSitesEssentialStructures = function() { 52 | return this.find(FIND_CONSTRUCTION_SITES, {filter: (object) => [STRUCTURE_SPAWN, STRUCTURE_TOWER, STRUCTURE_LINK, STRUCTURE_EXTENSION].includes(object.structureType)}); 53 | }; 54 | 55 | Room.prototype.findSources = function() { 56 | return this.find(FIND_SOURCES); 57 | }; 58 | 59 | Room.prototype.findMinerals = function() { 60 | return this.find(FIND_MINERALS); 61 | }; 62 | 63 | Room.prototype.findSpawnsNotSpawning = function() { 64 | return this.find(FIND_MY_SPAWNS, { 65 | filter: (spawn) => !spawn.spawning && getCachedIsActive(spawn), 66 | }); 67 | }; 68 | 69 | Room.prototype.findNukes = function() { 70 | return this.find(FIND_NUKES); 71 | }; 72 | 73 | Room.prototype.findEnemies = function() { 74 | return this.find(FIND_HOSTILE_CREEPS, { 75 | // TODO `brain.isFriend` extract to a module and import via require 76 | filter: (object) => !isFriend(object.owner.username), 77 | }); 78 | }; 79 | 80 | Room.prototype.findAlliedCreepsToHeal = function() { 81 | return this.find(FIND_HOSTILE_CREEPS, { 82 | filter: (object) => { 83 | if (object.hits === object.hitsMax) { 84 | return false; 85 | } 86 | if (isFriend(object.owner.username)) { 87 | return true; 88 | } 89 | return false; 90 | }, 91 | }); 92 | }; 93 | 94 | Room.prototype.findMyCreepsToHeal = function() { 95 | return this.find(FIND_MY_CREEPS, { 96 | filter: (object) => object.hits < object.hitsMax, 97 | }); 98 | }; 99 | 100 | Room.prototype.findHostileAttackingCreeps = function() { 101 | return this.find(FIND_HOSTILE_CREEPS, { 102 | // TODO unify the filter better 103 | filter: this.findAttackCreeps, 104 | }); 105 | }; 106 | 107 | Room.prototype.findInvaderCore = function() { 108 | return this.find(FIND_HOSTILE_STRUCTURES, { 109 | filter: {structureType: STRUCTURE_INVADER_CORE}, 110 | }, 111 | ); 112 | }; 113 | 114 | Room.prototype.findStructuresOfStructureType = function(structureType) { 115 | return this.find(FIND_STRUCTURES, { 116 | filter: {structureType: structureType}, 117 | }, 118 | ); 119 | }; 120 | 121 | Room.prototype.findMyCreepsOfRole = function(role) { 122 | return this.find(FIND_MY_CREEPS, { 123 | filter: (object) => object.memory && object.memory.role === role, 124 | }); 125 | }; 126 | 127 | Room.prototype.findSourceKeepersStructures = function() { 128 | return this.find(FIND_HOSTILE_STRUCTURES, { 129 | filter: (object) => object.owner.username === 'Source Keeper', 130 | }); 131 | }; 132 | 133 | Room.prototype.findDefenseStructures = function() { 134 | return this.find(FIND_STRUCTURES, { 135 | filter: (structure) => { 136 | return [STRUCTURE_WALL, STRUCTURE_RAMPART].indexOf(structure.structureType) > -1; 137 | }, 138 | }); 139 | }; 140 | 141 | Room.prototype.findPowerBanks = function() { 142 | return this.find(FIND_STRUCTURES, { 143 | filter: {structureType: STRUCTURE_POWER_BANK}, 144 | }); 145 | }; 146 | 147 | Room.prototype.findDestructibleStructures = function() { 148 | return this.find(FIND_STRUCTURES, { 149 | filter: (structure) => { 150 | if ([STRUCTURE_CONTROLLER, STRUCTURE_ROAD, STRUCTURE_CONTAINER, STRUCTURE_INVADER_CORE].includes(structure.structureType)) { 151 | return false; 152 | } 153 | if (!structure.hitsMax) { 154 | return false; 155 | } 156 | return true; 157 | }, 158 | }); 159 | }; 160 | 161 | Room.prototype.findTowers = function() { 162 | return this.find(FIND_STRUCTURES, {filter: {structureType: STRUCTURE_TOWER}}); 163 | }; 164 | 165 | Room.prototype.findLabs = function() { 166 | return this.find(FIND_STRUCTURES, {filter: {structureType: STRUCTURE_LAB}}); 167 | }; 168 | -------------------------------------------------------------------------------- /src/role_reserver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * reserver is used to reserve controller in external harvesting rooms 5 | * 6 | * Moves to the controller and reserves 7 | * Currently checks if there are enough sourcer and maybe trigger a defender. 8 | */ 9 | 10 | roles.reserver = {}; 11 | roles.reserver.killPrevious = true; 12 | // TODO should be true, but flee must be fixed (2016-10-13) 13 | roles.reserver.flee = false; 14 | 15 | roles.reserver.settings = { 16 | layoutString: 'MK', 17 | maxLayoutAmount: 1, 18 | }; 19 | roles.reserver.updateSettings = function(room, creep) { 20 | // room.debugLog('reserver', `role_reserver.updateSettings; targetRoom: ${creep.routing.targetRoom}`); 21 | const targetRoom = Game.rooms[creep.routing.targetRoom]; 22 | if (targetRoom) { 23 | const reservation = targetRoom.controller.reservation; 24 | if (reservation) { 25 | const maxReserveTicks = CONTROLLER_RESERVE_MAX - reservation.ticksToEnd; 26 | // E.g. ticksToEnd: 3300, RESERVE_MAX: 5000, LIFE_TIME 500 27 | // maxReserveTicks = 1700 28 | // maxClaimParts = 4 (1 to keep up the ticking + 3 to increase the ticksToEnd) 29 | const maxClaimParts = Math.ceil(maxReserveTicks / CREEP_CLAIM_LIFE_TIME); 30 | room.debugLog('reserver', `role_reserver.updateSettings - maxReserveTicks: ${maxReserveTicks} life_time: ${CREEP_CLAIM_LIFE_TIME} maxClaimParts: ${maxClaimParts}`); 31 | return { 32 | maxLayoutAmount: maxClaimParts, 33 | }; 34 | } 35 | } 36 | // room.debugLog('reserver', `role_reserver.updateSettings - Can not access targetRoom ${targetRoom}`); 37 | return { 38 | maxLayoutAmount: 1, 39 | }; 40 | }; 41 | 42 | /** 43 | * callCleaner 44 | * 45 | * @param {object} creep 46 | * @return {boolean} 47 | */ 48 | function callCleaner(creep) { 49 | if (creep.inBase()) { 50 | return false; 51 | } 52 | 53 | if (!Game.rooms[creep.memory.base].storage) { 54 | return false; 55 | } 56 | 57 | if (!creep.room.executeEveryTicks(1000)) { 58 | return false; 59 | } 60 | 61 | if (config.creep.structurer) { 62 | callStructurer(creep); 63 | } 64 | } 65 | 66 | /** 67 | * callStructurer 68 | * 69 | * @param {object} creep 70 | */ 71 | function callStructurer(creep) { 72 | if (creep.room.isMy()) { 73 | creep.log(`Calling structurer with my room`); 74 | return; 75 | } 76 | const resourceStructures = creep.room.findDestructibleStructures(); 77 | if (resourceStructures.length > 0) { 78 | creep.log('Call structurer from ' + creep.memory.base + ' because of ' + resourceStructures[0].structureType); 79 | Game.rooms[creep.memory.base].checkRoleToSpawn('structurer', 1, undefined, creep.room.name); 80 | } 81 | } 82 | 83 | /** 84 | * interactWithControllerSuccess 85 | * 86 | * @param {object} creep 87 | */ 88 | function interactWithControllerSuccess(creep) { 89 | if (creep.room.controller.reservation) { 90 | creep.room.data.reservation = { 91 | base: creep.memory.base, 92 | tick: Game.time, 93 | ticksToLive: creep.ticksToLive, 94 | ticksToEnd: creep.room.controller.reservation.ticksToEnd, 95 | }; 96 | } 97 | } 98 | 99 | /** 100 | * interactWithController 101 | * 102 | * @param {object} creep 103 | * @return {boolean} 104 | */ 105 | function interactWithController(creep) { 106 | let returnCode; 107 | if (creep.room.controller.owner && creep.room.controller.owner.username !== Memory.username) { 108 | creep.say('attack'); 109 | returnCode = creep.attackController(creep.room.controller); 110 | } else if (creep.room.controller.reservation && creep.room.controller.reservation.username !== Memory.username) { 111 | creep.say('unreserve'); 112 | returnCode = creep.attackController(creep.room.controller); 113 | } else { 114 | returnCode = creep.reserveController(creep.room.controller); 115 | } 116 | 117 | if (returnCode === OK || returnCode === ERR_NO_BODYPART) { 118 | interactWithControllerSuccess(creep); 119 | return true; 120 | } 121 | if (returnCode === ERR_NOT_IN_RANGE) { 122 | return true; 123 | } 124 | if (returnCode === ERR_INVALID_TARGET) { 125 | return true; 126 | } 127 | 128 | creep.log('reserver: ' + returnCode); 129 | } 130 | 131 | /** 132 | * callDefender 133 | * 134 | * @param {object} creep 135 | */ 136 | function callDefender(creep) { 137 | const hostiles = creep.room.findEnemies(); 138 | const invaderCores = creep.room.find(FIND_STRUCTURES, {filter: {structureType: STRUCTURE_INVADER_CORE}}); 139 | if (hostiles.length > 0 || invaderCores.length > 0) { 140 | // this.log('Reserver under attack'); 141 | if (!creep.memory.defender_called) { 142 | Game.rooms[creep.memory.base].memory.queue.push({ 143 | role: 'defender', 144 | routing: { 145 | targetRoom: creep.room.name, 146 | }, 147 | }); 148 | creep.memory.defender_called = true; 149 | } 150 | } 151 | } 152 | 153 | 154 | roles.reserver.action = function(creep) { 155 | creep.mySignController(); 156 | creep.setNextSpawn(); 157 | if (creep.room.data.state !== 'Controlled') { 158 | creep.spawnReplacement(); 159 | } 160 | 161 | callCleaner(creep); 162 | 163 | if (config.creep.reserverDefender) { 164 | callDefender(creep); 165 | } 166 | 167 | interactWithController(creep); 168 | return true; 169 | }; 170 | -------------------------------------------------------------------------------- /doc/Design.md: -------------------------------------------------------------------------------- 1 | # Design 2 | 3 | The AI automatically generates a layout for the room and builds the structures 4 | for the current RCL. A `scout` creep or observer explores external harvest rooms. 5 | If the current number of rooms is less than the GCL, the AI will acquire new 6 | rooms. Fallen rooms will be survived and basically defended. The AI sends some waves of 7 | auto attacks. Minerals are fetched from the extractor and transported to the 8 | terminal. Reactions are basically implemented. Depending on a threshold minerals 9 | are sold on the market. 10 | 11 | ## Trapped Detection System 12 | 13 | The AI includes a trapped scenario detection system that identifies when the bot is imprisoned by hostile players and unable to expand naturally. 14 | 15 | ### Detection Logic 16 | 17 | - **Qualification**: GCL ≥ 3 and only one controlled room 18 | - **Stagnation Threshold**: 50,000 ticks (~7 days) of single-room state 19 | - **Hostile Analysis**: Checks adjacent rooms for enemy control/reservation 20 | - **Resource Validation**: Excludes legitimate CPU/heap/memory constraints 21 | 22 | ### Configuration 23 | 24 | All settings are configurable via `config.trapped` in `src/config.js`: 25 | - `enabled`: Enable/disable the system 26 | - `minimumGCL`: Minimum GCL required for detection 27 | - `stagnationThreshold`: Ticks before considering trapped 28 | - `checkInterval`: Analysis frequency (performance optimization) 29 | - `logInterval`: Status logging frequency 30 | - `debugLogging`: Enable debug output 31 | 32 | ### Integration 33 | 34 | The system sets `global.data.isTrapped` flag for other systems to use and provides: 35 | - `Memory.trapped.isTrapped`: Boolean trapped state 36 | - Regular console logging for recognition 37 | - Automatic state clearing when conditions change 38 | 39 | ## [Base building](BaseBuilding.md) 40 | 41 | ### Logic 42 | 43 | The number of structures are checked and if applicable new constructionSites 44 | are places. Links are triggered to transfer energy to link near the storage. 45 | Towers attack incoming creeps or heal my creeps. If no spawn is available 46 | `nextroomer` from other rooms are called, to build up the room. 47 | 48 | The basic creep is the `universal` which can make sure, that enough energy 49 | will be available to build the rest of the creeps. For this we check if 50 | a `universal` is within the room, otherwise spawn it. For the rest a priority 51 | queue is used. 52 | 53 | 54 | ## Role 55 | 56 | - `upgrader` get energy from the storage, puts it into the controller. 57 | - `filler` get energy from a link and transfers it to the tower or storage. 58 | - `sourcer` get energy from source. 59 | - Controlled room: Transfers the energy to the link. 60 | - External room: Builds container, fills container, calls `carry` to get 61 | the energy. 62 | - `reserver` reservs an external controller and calls `sourcer`. 63 | - `carry` 64 | A carry transports energy from the target to the storage in the base (From sources, energy piles or storages in other rooms) 65 | They use fixed precalculated paths 66 | While moving they try to transfer energy to the next creep or if in base to other structures (tower, link, extension) 67 | So carries move forward if they have 'no' energy, backwards if the 'have' energy 68 | 'no' or 'have' is defined differently for different rooms 69 | 70 | The idea behind that is: 71 | - If the carry is in the target room, they should have a proper amount of energy to move all the way back 72 | - If the carry is in an intermediate room, they should have at least a mediocre amount of energy to move back 73 | - If the carry is in the base it should empty itself before moving backansfered. 74 | 75 | Carries are spawned by: 76 | - sourcer: if the energy pile or container is over a certain threshold 77 | - structurer: if the enrgy pile is over a certain threshold 78 | - rooms: if the storage is below a certain threshold to get energy from another room 79 | - `scout` Breadth-first search based room exploring. 80 | - `universal` moves on the universal path, and transfers energy to free structures 81 | on the path. On low energy in storage, the `universal` falls back to the 82 | start up phase without relying on anything (storage, links, other creeps). 83 | - `nextroomer` moves to target room and builds up that room. 84 | - `repairer` build walls and ramparts. 85 | - `builder` Builds construction sites 86 | 87 | 88 | ## Routing 89 | 90 | The routing from `start` to `end` is first done on room level: 91 | 92 | - Find `Game.map.findRoute(start, end)` plus the start room added as first 93 | entry in the array. This is stored together with the `routePos` in memory 94 | of the creep. 95 | - Inside a single room: 96 | - First room (`routePos == 0`): Own rooms have a layout set with a path to 97 | each exit pre-calculated. The first part of the path name is `Start` and 98 | the second the room to move to. 99 | - Last room (`routePos == route.length -1`): the `targetId` is stored in the 100 | memory of the creep. So the first part of the path name is the previous 101 | room and the second part is the `targetId`. 102 | - Rooms on the path: The previous room is the first part of the path name, 103 | the next room is the second part of the path name. 104 | The path is cached in the memory of the room with a `created` attributes 105 | to allow invalidation. 106 | -------------------------------------------------------------------------------- /src/prototype_room_memory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * Memory abstraction layer 5 | * 6 | * The methods build the interface to the memory. 7 | * Path: 8 | * The idea is to store paths as roomPosition in the global data object. 9 | * Store paths from my rooms in a compressed form in memory. 10 | * 11 | * CostMatrix: 12 | * Store costMatrix for myRooms in data and memory. 13 | * Other rooms are only stored in data. 14 | */ 15 | 16 | /** 17 | * Deletes room memory 18 | */ 19 | Room.prototype.clearMemory = function() { 20 | this.memory = { 21 | invalidated: Game.time, 22 | }; 23 | }; 24 | 25 | /** 26 | * Returns the costMatrix for the room. 27 | * 28 | * @return {CostMatrix|undefined} 29 | */ 30 | Room.prototype.getMemoryCostMatrix = function() { 31 | return this.data.costMatrix; 32 | }; 33 | 34 | /** 35 | * Stores the costMatrix 36 | * 37 | * @param {Object} costMatrix - the costMatrix to save 38 | */ 39 | Room.prototype.setMemoryCostMatrix = function(costMatrix) { 40 | this.data.costMatrix = costMatrix; 41 | this.memory.costMatrix = costMatrix.serialize(); 42 | }; 43 | 44 | Room.prototype.populatePathToDataFromMemory = function(pathName) { 45 | const path = Room.stringToPath(this.memory.routing[pathName].path); 46 | console.log('-------------'); 47 | console.log(this.data.routing); 48 | console.log(this.memory.routing[pathName].path); 49 | console.log(path); 50 | this.data.routing[pathName] = { 51 | path: path, 52 | created: this.memory.routing[pathName].created, 53 | fixed: this.memory.routing[pathName].fixed, 54 | name: this.memory.routing[pathName].name, 55 | }; 56 | }; 57 | 58 | /** 59 | * Returns all paths for this room from data. If no path in data, populate it 60 | * 61 | * @return {object} 62 | */ 63 | Room.prototype.getMemoryPaths = function() { 64 | // if (!this.data.routing) { 65 | // const pathNames = Object.keys(this.memory.routing).sort(); 66 | // for (const pathName of pathNames) { 67 | // this.populatePathToDataFromMemory(pathName); 68 | // } 69 | // } 70 | return this.data.routing; 71 | }; 72 | 73 | /** 74 | * Returns the path for the given name. Checks for validity and populated 75 | * data if missing. 76 | * 77 | * @param {String} name - the name of the path 78 | * @return {array|boolean} path 79 | */ 80 | Room.prototype.getMemoryPath = function(name) { 81 | const isValid = function(path) { 82 | return path.fixed || path.created > Game.time - config.path.refresh; 83 | }; 84 | 85 | if (this.data.routing[name] && isValid(this.data.routing[name])) { 86 | return this.data.routing[name].path; 87 | } 88 | 89 | if (this.isMy()) { 90 | if (!this.memory.routing) { 91 | this.memory.routing = {}; 92 | } 93 | if (this.memory.routing[name] && isValid(this.memory.routing[name])) { 94 | this.populatePathToDataFromMemory(name); 95 | return this.data.routing[name].path; 96 | } 97 | } 98 | return false; 99 | }; 100 | 101 | /** 102 | * Cleans memory from all paths 103 | */ 104 | Room.prototype.deleteMemoryPaths = function() { 105 | delete this.memory.routing; 106 | }; 107 | 108 | /** 109 | * Removes the named path from memory 110 | * 111 | * @param {String} name - the name of the path 112 | */ 113 | Room.prototype.deleteMemoryPath = function(name) { 114 | delete this.memory.routing[name]; 115 | }; 116 | 117 | /** 118 | * Stores the given path under the name in data. If `fixed` is true 119 | * the path is stored in memory serialized. 120 | * 121 | * @param {String} name - the name of the path 122 | * @param {Array} path - the path itself 123 | * @param {boolean} perturb - Flag to define if the path should be perturbed 124 | */ 125 | Room.prototype.setMemoryPath = function(name, path, perturb = false) { 126 | if (perturb) { 127 | path = Room.perturbPath(path); 128 | } 129 | const item = { 130 | path: path, 131 | created: Game.time, 132 | name: name, 133 | }; 134 | this.data.routing[name] = item; 135 | }; 136 | 137 | /** 138 | * Bends orthogonal path segments into diagonal zigzags 139 | * 140 | * @param {Array} path - the path to perturb 141 | * @return {Array} changed - the perturbed path 142 | */ 143 | Room.perturbPath = function(path) { 144 | if (!path) { 145 | return path; 146 | } 147 | let skip = false; 148 | let prevDir = null; 149 | let prevDirOffset = 2; 150 | for (let pathIndex = 0; pathIndex < path.length - 1; pathIndex++) { 151 | const posPathObject = path[pathIndex]; 152 | const posPathNext = path[pathIndex + 1]; 153 | const dirNext = posPathObject.getDirectionTo(posPathNext); 154 | if (skip) { 155 | // don't perturb if we did on the last step 156 | skip = false; 157 | prevDir = dirNext; 158 | continue; 159 | } 160 | if (prevDir !== dirNext || dirNext % 2 === 0) { 161 | // don't perturb corners or diagonals 162 | prevDir = dirNext; 163 | continue; 164 | } 165 | for (let dirOffset = -prevDirOffset; dirOffset !== prevDirOffset * 3; dirOffset += prevDirOffset * 2) { 166 | const offsetPosition = posPathObject.getAdjacentPosition((dirNext + dirOffset + 7) % 8 + 1); 167 | if (offsetPosition.lookFor(LOOK_TERRAIN)[0] === 'plain' && !offsetPosition.inPositions() && !offsetPosition.isBorder(1)) { 168 | path[pathIndex] = offsetPosition; 169 | prevDirOffset = dirOffset; 170 | skip = true; 171 | break; 172 | } 173 | } 174 | prevDir = dirNext; 175 | } 176 | return path; 177 | }; 178 | -------------------------------------------------------------------------------- /src/brain_stats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | brain.stats.init = function() { 4 | const userName = Memory.username; 5 | if (!config.stats.enabled || !userName) { 6 | return false; 7 | } 8 | Memory.stats = {[userName]: {roles: {}, room: {}}}; 9 | const rolesNames = _(Game.creeps).map((c) => c.memory.role).countBy((r) => { 10 | return r; 11 | }).value(); 12 | _.each(rolesNames, (element, index) => { 13 | Memory.stats[userName].roles[index] = element; 14 | }); 15 | 16 | if (config.cpuStats.enabled) { 17 | const startCpuWith = {load: global.load, time: Game.time, bucket: Game.cpu.bucket, tickLimit: global.tickLimit}; 18 | Memory.cpuStats = { 19 | start: startCpuWith, 20 | last: startCpuWith, 21 | summary: {maxBucket: Game.cpu.bucket, maxLoad: global.load, minBucket: Game.cpu.bucket, runTime: 0}, 22 | }; 23 | } 24 | }; 25 | 26 | brain.stats.modifyRoleAmount = function(role, diff) { 27 | const userName = Memory.username; 28 | if (!config.stats.enabled || !userName) { 29 | return false; 30 | } 31 | if (Memory.stats && Memory.stats[userName] && Memory.stats[userName].roles) { 32 | const roleStat = Memory.stats[userName].roles[role]; 33 | const previousAmount = roleStat ? roleStat : 0; 34 | const amount = (diff < 0 && previousAmount < -diff) ? 0 : previousAmount + diff; 35 | brain.stats.add(['roles', role], amount); 36 | } else { 37 | brain.stats.init(); 38 | } 39 | }; 40 | 41 | /** 42 | * stats.add use for push anything into Memory.stats at a given place. 43 | * 44 | * @param {Array} path Sub stats path. 45 | * @param {Any} newContent The value to push into stats. 46 | * @return {boolean} 47 | */ 48 | brain.stats.add = function(path, newContent) { 49 | if (!config.stats.enabled || Game.time % 3) { 50 | return false; 51 | } 52 | 53 | const userName = Memory.username; 54 | Memory.stats = Memory.stats || {}; 55 | Memory.stats[userName] = Memory.stats[userName] || {}; 56 | 57 | let current = Memory.stats[userName]; 58 | for (const item of path) { 59 | if (!current[item]) { 60 | current[item] = {}; 61 | } 62 | current = current[item]; 63 | } 64 | 65 | _.merge(current, newContent); 66 | return true; 67 | }; 68 | 69 | /** 70 | * stats.addRoot sets the root values, cpu, exec, gcl 71 | * 72 | * @return {boolean} 73 | */ 74 | brain.stats.addRoot = function() { 75 | if (!config.stats.enabled || Game.time % 3) { 76 | return false; 77 | } 78 | brain.stats.add([], { 79 | cpu: { 80 | limit: Game.cpu.limit, 81 | tickLimit: Game.cpu.tickLimit, 82 | bucket: Game.cpu.bucket, 83 | }, 84 | exec: { 85 | halt: Game.cpu.bucket < Game.cpu.tickLimit * 2, 86 | }, 87 | gcl: { 88 | level: Game.gcl.level, 89 | progress: Game.gcl.progress, 90 | progressTotal: Game.gcl.progressTotal, 91 | }, 92 | }); 93 | return true; 94 | }; 95 | 96 | /** 97 | * stats.addRoom call stats.add with given values and given sub room path. 98 | * 99 | * @param {String} roomName The room which from we will save stats. 100 | * @param {Number} previousCpu 101 | * @return {boolean} 102 | */ 103 | brain.stats.addRoom = function(roomName, previousCpu) { 104 | if (!config.stats.enabled || Game.time % 3) { 105 | return false; 106 | } 107 | 108 | const room = Game.rooms[roomName]; 109 | if (!room) { 110 | return false; 111 | } 112 | brain.stats.add(['room', roomName], { 113 | energy: { 114 | available: room.energyAvailable, 115 | capacity: room.energyCapacityAvailable, 116 | sources: _.sum(_.map(room.findSources(), 'energy')), 117 | }, 118 | controller: { 119 | progress: room.controller.progress, 120 | progressTotal: room.controller.progressTotal, 121 | }, 122 | creeps: { 123 | into: room.find(FIND_CREEPS).length, 124 | queue: Memory[room.name].queue.length, 125 | }, 126 | cpu: Game.cpu.getUsed() - previousCpu, 127 | }); 128 | 129 | if (room.storage) { 130 | const storage = room.storage; 131 | brain.stats.add(['room', roomName, 'storage'], { 132 | energy: storage.store.energy, 133 | power: storage.store.power, 134 | }); 135 | } 136 | if (room.terminal) { 137 | const terminal = room.terminal; 138 | brain.stats.add(['room', roomName, 'terminal'], { 139 | energy: terminal.store.energy, 140 | minerals: _.sum(terminal.store) - terminal.store.energy, 141 | }); 142 | } 143 | return true; 144 | }; 145 | 146 | /** 147 | * cpuLimit 148 | * sigmoid on Game.cpu.limit + Game.cpu.bucket 149 | * 150 | * @return {number} 151 | */ 152 | function cpuLimit() { 153 | // https://en.wikipedia.org/wiki/Sigmoid_function 154 | const sigmoid = (x) => 1 + Math.tanh((2 * x) - 1); 155 | return _.ceil(Game.cpu.limit * sigmoid(Game.cpu.bucket / 10000)); 156 | } 157 | module.exports.cpuLimit = cpuLimit; 158 | 159 | brain.stats.updateCpuStats = function() { 160 | if (config.cpuStats.enabled) { 161 | Memory.cpuStats.last = { 162 | load: _.round(Game.cpu.getUsed()), 163 | time: Game.time, 164 | bucket: Game.cpu.bucket, 165 | tickLimit: cpuLimit(), 166 | }; 167 | Memory.cpuStats.summary = { 168 | maxBucket: Math.max(Memory.cpuStats.summary.maxBucket, Memory.cpuStats.last.bucket), 169 | maxLoad: Math.max(Memory.cpuStats.summary.maxLoad, Memory.cpuStats.last.load), 170 | minBucket: Math.min(Memory.cpuStats.summary.minBucket, Memory.cpuStats.last.bucket), 171 | runTime: Memory.cpuStats.last.time - Memory.cpuStats.start.time, 172 | }; 173 | } 174 | }; 175 | --------------------------------------------------------------------------------