├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── deploy.yml ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __mocks__ └── screeps-profiler.ts ├── docker-compose.yml ├── grafana └── Overview.json ├── jest.config.js ├── package-lock.json ├── package.json ├── reset-docker.ps1 ├── reset-docker.sh ├── rollup.config.js ├── screeps.sample.json ├── src ├── Behaviors │ ├── Behavior.ts │ ├── GetEnergy │ │ ├── energySourcesByOffice.ts │ │ ├── engineerGetEnergy.ts │ │ ├── fromDroppedResource.ts │ │ ├── fromFranchise.ts │ │ ├── fromSource.ts │ │ └── fromStorageStructure.ts │ ├── Labs │ │ ├── deposit.ts │ │ ├── emptyLabs.ts │ │ ├── fillLabs.ts │ │ └── withdrawResources.ts │ ├── Logistics │ │ ├── deposit.ts │ │ └── withdraw.ts │ ├── Movement │ │ └── moveByFlowfield.ts │ ├── accountantGetEnergy.ts │ ├── blinkyKill.ts │ ├── engineerGetEnergy.ts │ ├── followPathHomeFromSource.ts │ ├── getBoosted.ts │ ├── getEnergyFromFranchise.ts │ ├── getEnergyFromLink.ts │ ├── getEnergyFromRuin.ts │ ├── getEnergyFromSource.ts │ ├── getEnergyFromStorage.ts │ ├── getEnergyFromStorageStructure.ts │ ├── getResourcesFromMineContainer.ts │ ├── guardKill.ts │ ├── harvestEnergyFromFranchise.ts │ ├── recycle.ts │ ├── signRoom.ts │ ├── stateMachine.ts │ └── states.ts ├── DEBUG_reset_Memory.js ├── Intel │ ├── Franchises │ │ ├── updateFranchisePaths.ts │ │ └── updateLedger.ts │ ├── Rooms │ │ ├── initializeOfficeMemory.ts │ │ ├── initializeRoomMemory.ts │ │ ├── mineralQuotas.ts │ │ ├── purgeDeadOffices.ts │ │ └── refreshRoomMemory.ts │ ├── events.ts │ └── index.ts ├── Ledger │ ├── AccuracyLedger.ts │ ├── AcquireLedger.ts │ ├── HarvestLedger.ts │ ├── LogisticsLedger.ts │ └── index.ts ├── Metrics │ ├── heapMetrics.ts │ └── recordMetrics.ts ├── Minions │ ├── Builds │ │ ├── accountant.ts │ │ ├── auditor.ts │ │ ├── blinky.ts │ │ ├── clerk.ts │ │ ├── dismantler.ts │ │ ├── engineer.ts │ │ ├── foreman.ts │ │ ├── guard.ts │ │ ├── lawyer.ts │ │ ├── marketer.ts │ │ ├── medic.ts │ │ ├── powerbank.ts │ │ ├── research.ts │ │ ├── salesman.ts │ │ └── utils.ts │ ├── bestBuildTier.ts │ ├── engineer_demo.js │ ├── minionTypes.ts │ ├── pbd_demo.js │ ├── runSpawns.ts │ └── spawnQueues.ts ├── Missions │ ├── BaseClasses │ │ ├── BaseDuoMission.ts │ │ ├── CreepSpawner │ │ │ ├── BaseCreepSpawner.ts │ │ │ ├── ConditionalCreepSpawner.ts │ │ │ ├── CreepSpawner.ts │ │ │ └── MultiCreepSpawner.ts │ │ ├── MissionImplementation.ts │ │ ├── MissionSpawner │ │ │ ├── BaseMissionSpawner.ts │ │ │ ├── ConditionalMissionSpawner.ts │ │ │ ├── MissionSpawner.ts │ │ │ └── MultiMissionSpawner.ts │ │ ├── index.ts │ │ └── runMissions.ts │ ├── Budgets.ts │ ├── Control.ts │ ├── Implementations │ │ ├── AcquireEngineerMission.ts │ │ ├── AcquireLawyerMission.ts │ │ ├── AcquireMission.ts │ │ ├── CleanupMission.ts │ │ ├── DefendAcquireMission.ts │ │ ├── DefendOfficeMission.ts │ │ ├── DefendRemoteMission.ts │ │ ├── DefenseCoordinationMission.ts │ │ ├── EmergencyUpgradeMission.ts │ │ ├── EngineerMission.ts │ │ ├── ExploreMission.ts │ │ ├── FastfillerMission.ts │ │ ├── HQLogisticsMission.ts │ │ ├── HarvestMission.ts │ │ ├── KillCoreMission.ts │ │ ├── LogisticsMission.ts │ │ ├── MainOfficeMission.ts │ │ ├── ManualAttackMission.ts │ │ ├── ManualSwarmTowerMission.ts │ │ ├── MineMission.ts │ │ ├── PlunderMission.ts │ │ ├── PowerBankDuoMission.ts │ │ ├── PowerBankMission.ts │ │ ├── ReserveMission.ts │ │ ├── ScienceMission.ts │ │ └── UpgradeMission.ts │ ├── MemoryFixes.ts │ ├── Mission.ts │ ├── Selectors │ │ └── index.ts │ └── initializeOfficeMissions.ts ├── Reports │ ├── AcquireReport.ts │ ├── BudgetReport.ts │ ├── EnergyReport.ts │ ├── FacilitiesReport.ts │ ├── FranchiseReport.ts │ ├── LabsReport.ts │ ├── MarketReport.ts │ ├── MilestonesReport.ts │ ├── MissionsReport.ts │ ├── OfficeReport.ts │ ├── PowerReport.ts │ ├── RemotesReport.ts │ ├── ReportRunner.ts │ ├── RoomPlanningReport.ts │ ├── SpawnReport.ts │ ├── TerminalsReport.ts │ ├── TerritoriesReport.ts │ └── fastfillerPositions.ts ├── RoomPlanner │ ├── Algorithms │ │ ├── distancetransform.ts │ │ ├── flowfield.ts │ │ ├── mincut.d.ts │ │ └── mincut.js │ ├── Backfill │ │ ├── BackfillPlan.ts │ │ ├── deserializeBackfillPlan.ts │ │ └── validateBackfillPlan.ts │ ├── EngineerQueue.ts │ ├── Extensions │ │ ├── ExtensionsPlan.ts │ │ ├── deserializeExtensionsPlan.ts │ │ └── validateExtensionsPlan.ts │ ├── Franchise │ │ ├── FranchisePlan.ts │ │ ├── deserializeFranchisePlan.ts │ │ ├── serializeFranchisePlan.ts │ │ └── validateFranchisePlan.ts │ ├── Library │ │ ├── LibraryPlan.ts │ │ ├── deserializeLibraryPlan.ts │ │ └── validateLibraryPlan.ts │ ├── Mine │ │ ├── MinePlan.ts │ │ ├── deserializeMinePlan.ts │ │ └── validateMinePlan.ts │ ├── Perimeter │ │ ├── PerimeterPlan.ts │ │ ├── deserializePerimeterPlan.ts │ │ └── validatePerimeterPlan.ts │ ├── PlannedStructure.ts │ ├── Roads │ │ ├── RoadsPlan.ts │ │ ├── deserializeRoadsPlan.ts │ │ └── validateRoadsPlan.ts │ ├── RoomArchitect.ts │ ├── Stamps │ │ ├── deserializeFastfillerPlan.ts │ │ ├── deserializeHeadquartersPlan.ts │ │ ├── deserializeLabsPlan.ts │ │ ├── fitStamp.ts │ │ ├── planMainStamps.ts │ │ ├── pointsOfInterest.ts │ │ ├── stamps.ts │ │ ├── validateFastfillerPlan.ts │ │ ├── validateHeadquartersPlan.ts │ │ └── validateLabsPlan.ts │ ├── TerritoryFranchise.ts │ ├── index.ts │ ├── planRooms.ts │ └── scanRoomPlanStructures.ts ├── Selectors │ ├── Combat │ │ ├── combatStats.ts │ │ ├── costMatrixes.ts │ │ ├── defenseRamparts.ts │ │ ├── priorityTarget.ts │ │ ├── shouldPlunder.ts │ │ ├── threatAnalysis.ts │ │ └── towerDamage.ts │ ├── Franchises │ │ ├── franchiseActive.ts │ │ ├── franchiseDefenseRooms.ts │ │ ├── franchiseDisabled.ts │ │ ├── franchiseEnergyAvailable.ts │ │ ├── franchiseIncome.ts │ │ ├── franchiseIsFull.ts │ │ ├── franchiseThatNeedsEngineers.ts │ │ ├── franchisesByOffice.ts │ │ ├── franchisesThatNeedRoadWork.ts │ │ ├── furthestActiveFranchiseRoundTripDistance.ts │ │ ├── getFranchiseDistance.ts │ │ ├── harvestMissionsByOffice.ts │ │ ├── planFranchisePath.ts │ │ └── remoteFranchises.ts │ ├── Logistics │ │ └── predictiveCapacity.ts │ ├── Map │ │ ├── MapCoordinates.ts │ │ ├── Pathing.ts │ │ └── getRoomPathDistance.ts │ ├── Market │ │ ├── boostCost.ts │ │ └── marketPrice.ts │ ├── Missions │ │ ├── missionEnergyAvailable.ts │ │ └── updateMissionEnergyAvailable.ts │ ├── Structures │ │ ├── costForStructure.ts │ │ ├── destroyUnplannedStructures.ts │ │ └── repairThreshold.ts │ ├── boostQuotas.ts │ ├── boostsToSupply.ts │ ├── byId.ts │ ├── carryPartsForFranchiseRoute.ts │ ├── costMatrixFromRoomPlan.ts │ ├── costToBoostMinion.ts │ ├── cpuOverhead.ts │ ├── creepCounter.ts │ ├── creepStats.ts │ ├── defaultDirectionsForSpawn.ts │ ├── displayBucket.ts │ ├── energyInProduction.ts │ ├── energyInTransit.ts │ ├── findAlliedCreeps.ts │ ├── findHostileCreeps.ts │ ├── findReserveTargets.ts │ ├── getActualEnergyAvailable.ts │ ├── getExitTiles.ts │ ├── getExtensionsCapacity.ts │ ├── getHqLocations.ts │ ├── getLabs.ts │ ├── getOfficeDistance.ts │ ├── getPatrolRoute.ts │ ├── getPrimarySpawn.ts │ ├── getScientists.ts │ ├── getSpawnCost.ts │ ├── getTerritoriesByOffice.ts │ ├── hasEnergyIncome.ts │ ├── ingredientForLabsObjective.ts │ ├── ingredientsNeededForLabOrder.ts │ ├── isSpawned.ts │ ├── labsShouldBeEmptied.ts │ ├── linkUsedCapacity.ts │ ├── logCostMatrix.ts │ ├── marketEnabled.ts │ ├── minionCostPerTick.ts │ ├── missionCpuAvailable.ts │ ├── missionEnergyAvailable.ts │ ├── officeIsDownleveled.ts │ ├── officeResourceSurplus.ts │ ├── officeShouldMine.ts │ ├── ownedMinerals.ts │ ├── perimeter.ts │ ├── plannedActiveFranchiseRoads.ts │ ├── plannedFranchiseRoads.ts │ ├── plannedStructures.ts │ ├── plannedStructuresByRcl.ts │ ├── posById.ts │ ├── prespawn.ts │ ├── rcl.ts │ ├── reducers.ts │ ├── renewCost.ts │ ├── repairCostsPerTick.ts │ ├── reservations.ts │ ├── reserveCapacity.ts │ ├── resourcesNearPos.ts │ ├── roomCache.ts │ ├── roomIsEligibleForOffice.ts │ ├── roomPlans.ts │ ├── scheduledCallbacks.ts │ ├── shouldHandleBoosts.ts │ ├── spawnEnergyAvailable.ts │ ├── spawnsAndExtensionsDemand.ts │ ├── storageEnergyAvailable.ts │ ├── storageStructureThatNeedsEnergy.ts │ ├── terminalBalance.ts │ ├── territoryIntent.ts │ ├── towerDamage.ts │ ├── typeguards.ts │ ├── validatePathsToPointsOfInterest.ts │ └── viz.ts ├── Strategy │ ├── Acquire │ │ ├── findAcquireTarget.ts │ │ └── scoreAcquireTarget.ts │ ├── ResourceAnalysis │ │ ├── PowerBank.ts │ │ └── Selectors.ts │ ├── Territories │ │ └── HarassmentZones.ts │ └── constants.ts ├── Structures │ ├── Labs.ts │ ├── Labs │ │ ├── LabOrder.ts │ │ ├── Labs.ts │ │ ├── getLabOrderDependencies.ts │ │ ├── labsToEmpty.ts │ │ └── planLabOrders.ts │ ├── Links.ts │ ├── Observer.ts │ ├── PowerSpawn.ts │ ├── Ramparts.ts │ ├── Terminal.ts │ ├── Towers.ts │ └── index.ts ├── TaskManager.ts ├── config.ts ├── gameConstants.ts ├── gameLoop.ts ├── main.ts ├── types.d.ts └── utils │ ├── CityNames.ts │ ├── ResetMemoryOnRespawn.ts │ ├── RoomVisual.js │ ├── cleanUpCreeps.ts │ ├── debugCPU.ts │ ├── dump.ts │ ├── initializeSpawns.ts │ ├── logCPU.ts │ ├── memhack.ts │ ├── memoize.ts │ ├── memoizeFunction.ts │ ├── packrat.ts │ ├── profiler.ts │ ├── visualizeCostMatrix.ts │ └── visualizeRoomCluster.ts └── tsconfig.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/typescript-node:14-bullseye" 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | // "features": {}, 10 | 11 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 12 | // "forwardPorts": [], 13 | 14 | // Use 'postCreateCommand' to run commands after the container is created. 15 | // "postCreateCommand": "yarn install", 16 | 17 | // Configure tool-specific properties. 18 | // "customizations": {}, 19 | 20 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 21 | // "remoteUser": "root" 22 | } 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.py] 13 | indent_style = space 14 | indent_size = 4 15 | 16 | [*.rb] 17 | indent_style = space 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Build 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [master] 10 | pull_request: 11 | branches: [master] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 23 | - uses: actions/checkout@v2 24 | 25 | # Runs a single command using the runners shell 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | # Runs a set of commands using the runners shell 30 | - name: Run build process 31 | run: npm run build 32 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Deploy 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [master] 10 | 11 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 12 | jobs: 13 | # This workflow contains a single job called "build" 14 | build: 15 | # The type of runner that the job will run on 16 | runs-on: ubuntu-latest 17 | 18 | # Steps represent a sequence of tasks that will be executed as part of the job 19 | steps: 20 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 21 | - uses: actions/checkout@v2 22 | 23 | # Runs a single command using the runners shell 24 | - name: Install dependencies 25 | run: npm ci 26 | 27 | # Runs a set of commands using the runners shell 28 | - name: Deploy to Main 29 | env: 30 | SCREEPS_CONFIG: ${{secrets.SCREEPS_CONFIG}} 31 | run: npm run push-main 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # yarn lock file 2 | /yarn.lock 3 | 4 | # Ignore basic folders 5 | /dist 6 | /.rpt2_cache 7 | /tsc-out 8 | /node_modules 9 | /_book 10 | /build/* 11 | 12 | # TypeScript definitions installed by Typings 13 | /typings 14 | 15 | # Screeps Config 16 | screeps.json 17 | .screeps.yaml 18 | .env 19 | config.yml 20 | .screeps-multimeter.history 21 | 22 | # ScreepsServer data from integration tests 23 | /server 24 | 25 | # Numerous always-ignore extensions 26 | *.diff 27 | *.err 28 | *.orig 29 | *.log 30 | *.rej 31 | *.swo 32 | *.swp 33 | *.zip 34 | *.vi 35 | *~ 36 | 37 | # Editor folders 38 | .cache 39 | .project 40 | .settings 41 | .tmproj 42 | *.esproj 43 | nbproject 44 | *.sublime-project 45 | *.sublime-workspace 46 | .idea 47 | 48 | # ========================= 49 | # Operating System Files 50 | # ========================= 51 | 52 | # OSX 53 | # ========================= 54 | 55 | .DS_Store 56 | .AppleDouble 57 | .LSOverride 58 | 59 | # Thumbnails 60 | ._* 61 | 62 | # Files that might appear on external disk 63 | .Spotlight-V100 64 | .Trashes 65 | 66 | # Directories potentially created on remote AFP share 67 | .AppleDB 68 | .AppleDesktop 69 | Network Trash Folder 70 | Temporary Items 71 | .apdisk 72 | 73 | # Windows 74 | # ========================= 75 | 76 | # Windows image file caches 77 | Thumbs.db 78 | ehthumbs.db 79 | 80 | # Folder config file 81 | Desktop.ini 82 | 83 | # Recycle Bin used on file shares 84 | $RECYCLE.BIN/ 85 | 86 | # Windows Installer files 87 | *.cab 88 | *.msi 89 | *.msm 90 | *.msp 91 | 92 | # Windows shortcuts 93 | *.lnk 94 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "printWidth": 120, 5 | "singleQuote": true, 6 | "trailingComma": "none", 7 | "arrowParens": "avoid", 8 | "endOfLine": "auto" 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": {}, 3 | "[json]": {}, 4 | "[typescript]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | }, 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": true, 9 | "source.organizeImports": true 10 | }, 11 | "editor.formatOnSave": true, 12 | "editor.renderWhitespace": "all", 13 | "files.encoding": "utf8", 14 | "files.insertFinalNewline": true, 15 | "files.trimTrailingWhitespace": true, 16 | "search.exclude": { 17 | "_book/**": true, 18 | ".rpt2_cache/**": true, 19 | "dist/**": true, 20 | "node_modules/**": true, 21 | "typings/**": true 22 | }, 23 | "typescript.tsdk": "./node_modules/typescript/lib", 24 | "eslint.enable": true, 25 | "editor.tabSize": 2, 26 | "editor.defaultFormatter": "esbenp.prettier-vscode" 27 | } 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guidelines 2 | 3 | This document outlines guides to get started on developing the starter kit. 4 | 5 | ## Contributing to the docs 6 | 7 | Contributions to the docs are also welcome! We've documented the steps to do so [here](./docs/in-depth/contributing.md). 8 | 9 | ## The Five Golden Rules 10 | 11 | The simple steps of contributing to any GitHub project are as follows: 12 | 13 | 1. [Fork the repository](https://github.com/screepers/screeps-typescript-starter/fork) 14 | 2. Create your feature branch: `git checkout -b my-new-feature` 15 | 3. Commit your changes: `git commit -am 'Add some feature'` 16 | 4. Push to the branch: `git push -u origin my-new-feature` 17 | 5. Create a [Pull Request](https://github.com/screepers/screeps-typescript-starter/pulls)! 18 | 19 | To keep your fork of in sync with this repository, [follow this guide](https://help.github.com/articles/syncing-a-fork/). 20 | 21 | ## Submitting a pull request 22 | 23 | We accept almost all pull requests, as long as the following criterias are met: 24 | 25 | * Your code must pass all of the linter checks (`npm run lint`) 26 | * When adding a new feature, make sure it doesn't increase the complexity of the tooling. We want this starter kit to be approachable to folks who have little to no knowledge of TypeScript, or even JavaScript. 27 | * When making changes that are potentially breaking, careful discussion must be done with the community at large. Generally we do this either on the [#typescript](https://screeps.slack.com/messages/typecript/) channel on the Screeps Slack, or on the corresponding pull request discussion thread. 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=glitchassassin_screeps&metric=alert_status)](https://sonarcloud.io/dashboard?id=glitchassassin_screeps) 2 | 3 | # Screeps AI: Grey Company 4 | 5 | This code is based on the Screeps Typescript Starter. 6 | 7 | ## Deprecation Notice 8 | 9 | This code is in a fairly stable state, but active development has been moved to a private repo as combat and other capabilities become more complex. This repository will no longer be updated, but is left for reference in case any of the patterns are useful. 10 | 11 | ## Basic Usage 12 | 13 | For basic usage of the [screeps-typescript-starter](https://github.com/screepers/screeps-typescript-starter) framework, see that repo. 14 | -------------------------------------------------------------------------------- /__mocks__/screeps-profiler.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | enable: jest.fn(), 3 | wrap: jest.fn(), 4 | registerClass: jest.fn(), 5 | registerObject: jest.fn(), 6 | registerFN: jest.fn(), 7 | } 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | ## Clone screeps-launcher to the same parent directory. 2 | ## Then add the following to .env to include screeps-launcher services: 3 | ## COMPOSE_PATH_SEPARATOR=: 4 | ## COMPOSE_FILE=docker-compose.yml:../screeps-launcher/docker-compose.yml 5 | 6 | version: '3' 7 | services: 8 | # Add screeps-steamless-client to screeps-launcher 9 | client: 10 | image: node:16 11 | working_dir: /home/root/client 12 | command: sh -c 'npm install --log-level=error --no-progress laverdet/screeps-steamless-client && npx screeps-steamless-client --package /screeps.nw --host 0.0.0.0 --internal_backend http://screeps:21025 --backend http://localhost:21025' 13 | environment: 14 | SCREEPS_NW_PATH: ${SCREEPS_NW_PATH:?"Missing screeps nw file"} 15 | volumes: 16 | - ${SCREEPS_NW_PATH}:/screeps.nw 17 | 18 | ports: 19 | - 8080:8080/tcp 20 | restart: unless-stopped 21 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // An array of glob patterns indicating a set of files for which coverage information should be collected 4 | collectCoverageFrom: [ 5 | 'src/**/*.{js,ts}' 6 | ], 7 | 8 | // An array of regexp pattern strings used to skip coverage collection 9 | coveragePathIgnorePatterns: [ 10 | '/dist/', 11 | '\\.d\\.ts$', 12 | 'src/utils/ErrorMapper.ts' 13 | ], 14 | 15 | // An array of directory names to be searched recursively up from the requiring module's location 16 | moduleDirectories: [ 17 | 'node_modules', 18 | 'src' 19 | ], 20 | 21 | // A preset that is used as a base for Jest's configuration 22 | preset: 'ts-jest', 23 | 24 | // The test environment that will be used for testing 25 | testEnvironment: 'screeps-jest', 26 | 27 | // The glob patterns Jest uses to detect test files 28 | testMatch: [ 29 | '**/*.spec.ts', 30 | '!**/node_modules/**', 31 | '!**/dist/**' 32 | ] 33 | 34 | }; 35 | -------------------------------------------------------------------------------- /reset-docker.ps1: -------------------------------------------------------------------------------- 1 | docker compose exec screeps curl -X POST http://localhost:21026/cli -d 'system.resetAllData()' 2 | docker compose restart screeps 3 | sleep 15 4 | docker compose exec screeps curl -X POST http://localhost:21026/cli -d 'utils.addNPCTerminals()' 5 | docker compose exec screeps curl -X POST http://localhost:21026/cli -d 'system.pauseSimulation()' 6 | -------------------------------------------------------------------------------- /reset-docker.sh: -------------------------------------------------------------------------------- 1 | docker compose exec screeps curl -X POST http://localhost:21026/cli -d 'system.resetAllData()' 2 | docker compose restart screeps 3 | sleep 15 4 | docker compose exec screeps curl -X POST http://localhost:21026/cli -d 'utils.addNPCTerminals()' 5 | docker compose exec screeps curl -X POST http://localhost:21026/cli -d 'system.pauseSimulation()' 6 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import resolve from '@rollup/plugin-node-resolve'; 5 | import replace from '@rollup/plugin-replace'; 6 | import clear from 'rollup-plugin-clear'; 7 | import screeps from 'rollup-plugin-screeps'; 8 | import tsConfigPaths from 'rollup-plugin-tsconfig-paths'; 9 | import typescript from 'rollup-plugin-typescript2'; 10 | 11 | let cfg; 12 | const dest = process.env.DEST; 13 | const config = process.env.SCREEPS_CONFIG; 14 | if (config) { 15 | console.log('Loading config from environment variable'); 16 | cfg = JSON.parse(config); 17 | } else if (!dest) { 18 | console.log('No destination specified - code will be compiled but not uploaded'); 19 | } else if ((cfg = require('./screeps.json')[dest]) == null) { 20 | throw new Error('Invalid upload destination'); 21 | } 22 | 23 | export default { 24 | input: 'src/main.ts', 25 | output: { 26 | file: 'dist/main.js', 27 | format: 'cjs', 28 | sourcemap: true 29 | }, 30 | 31 | plugins: [ 32 | clear({ targets: ['dist'] }), 33 | replace({ 34 | preventAssignment: true, 35 | values: { 36 | __buildDate__: () => JSON.stringify(Date.now()) 37 | } 38 | }), 39 | tsConfigPaths(), 40 | resolve(), 41 | commonjs({ 42 | namedExports: { 43 | 'node_modules/class-transformer/index.js': ['Transform', 'Type', 'Exclude'] 44 | } 45 | }), 46 | typescript({ tsconfig: './tsconfig.json' }), 47 | screeps({ config: cfg, dryRun: cfg == null }) 48 | ] 49 | }; 50 | -------------------------------------------------------------------------------- /screeps.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": { 3 | "token": "YOUR_TOKEN", 4 | "protocol": "https", 5 | "hostname": "screeps.com", 6 | "port": 443, 7 | "path": "/", 8 | "branch": "main" 9 | }, 10 | "sim": { 11 | "token": "YOUR_TOKEN", 12 | "protocol": "https", 13 | "hostname": "screeps.com", 14 | "port": 443, 15 | "path": "/", 16 | "branch": "sim" 17 | }, 18 | "pserver": { 19 | "email": "username", 20 | "password": "Password", 21 | "protocol": "http", 22 | "hostname": "1.2.3.4", 23 | "port": 21025, 24 | "path": "/", 25 | "branch": "main" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Behaviors/Behavior.ts: -------------------------------------------------------------------------------- 1 | export enum BehaviorResult { 2 | SUCCESS = 'SUCCESS', 3 | FAILURE = 'FAILURE', 4 | INPROGRESS = 'INPROGRESS' 5 | } 6 | -------------------------------------------------------------------------------- /src/Behaviors/GetEnergy/fromDroppedResource.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorResult } from 'Behaviors/Behavior'; 2 | import { moveTo } from 'screeps-cartographer'; 3 | import { byId } from 'Selectors/byId'; 4 | 5 | export const fromDroppedResource = (creep: Creep, resourceId: Id) => { 6 | const resource = byId(resourceId); 7 | if (!resource) return BehaviorResult.FAILURE; 8 | 9 | moveTo(creep, resource); 10 | if (creep.pickup(resource) === OK) { 11 | return BehaviorResult.SUCCESS; 12 | } 13 | return BehaviorResult.INPROGRESS; 14 | }; 15 | -------------------------------------------------------------------------------- /src/Behaviors/GetEnergy/fromFranchise.ts: -------------------------------------------------------------------------------- 1 | import { moveTo } from 'screeps-cartographer'; 2 | import { franchiseEnergyAvailable } from 'Selectors/Franchises/franchiseEnergyAvailable'; 3 | import { posById } from 'Selectors/posById'; 4 | import { resourcesNearPos } from 'Selectors/resourcesNearPos'; 5 | import { getFranchisePlanBySourceId } from 'Selectors/roomPlans'; 6 | import { BehaviorResult } from '../Behavior'; 7 | 8 | /** 9 | * Returns SUCCESS if expected to complete this tick, FAILURE if it 10 | * cannot complete (or source is depleted), INPROGRESS otherwise 11 | */ 12 | export const fromFranchise = (creep: Creep, sourceId: Id) => { 13 | const sourcePos = posById(sourceId); 14 | if (!sourcePos) return BehaviorResult.FAILURE; 15 | if (!Game.rooms[sourcePos.roomName]) { 16 | moveTo(creep, { pos: sourcePos, range: 2 }); 17 | return BehaviorResult.INPROGRESS; 18 | } 19 | 20 | if (franchiseEnergyAvailable(sourceId) < 50) { 21 | return BehaviorResult.SUCCESS; 22 | } 23 | 24 | const container = getFranchisePlanBySourceId(sourceId)?.container.structure as StructureContainer | undefined; 25 | const resources = resourcesNearPos(sourcePos, 1, RESOURCE_ENERGY); 26 | let pickedUp = 0; 27 | if (container && container.store.getUsedCapacity(RESOURCE_ENERGY) > 0) { 28 | // Get from container, if possible 29 | moveTo(creep, { pos: container.pos, range: 1 }); 30 | if (creep.withdraw(container, RESOURCE_ENERGY) === OK) { 31 | pickedUp += container.store.getUsedCapacity(RESOURCE_ENERGY); 32 | } 33 | } else if (resources.length > 0) { 34 | // Otherwise, pick up loose resources 35 | const res = resources.shift(); 36 | if (res) { 37 | moveTo(creep, { pos: res.pos, range: 1 }); 38 | if (creep.pickup(res) === OK) { 39 | pickedUp += res.amount; 40 | } 41 | } 42 | } 43 | 44 | if (creep.store.getFreeCapacity(RESOURCE_ENERGY) <= pickedUp) { 45 | return BehaviorResult.SUCCESS; 46 | } 47 | 48 | return BehaviorResult.INPROGRESS; 49 | }; 50 | -------------------------------------------------------------------------------- /src/Behaviors/GetEnergy/fromSource.ts: -------------------------------------------------------------------------------- 1 | import { defaultRoomCallback } from 'Selectors/Map/Pathing'; 2 | import { byId } from 'Selectors/byId'; 3 | import { posById } from 'Selectors/posById'; 4 | import { moveTo } from 'screeps-cartographer'; 5 | import { BehaviorResult } from '../Behavior'; 6 | 7 | /** 8 | * Returns SUCCESS if expected to complete this tick, FAILURE if it 9 | * cannot complete (or source is depleted), INPROGRESS otherwise 10 | */ 11 | export const fromSource = (creep: Creep, sourceId: Id) => { 12 | const sourcePos = posById(sourceId); 13 | if (!sourcePos) return BehaviorResult.FAILURE; 14 | moveTo( 15 | creep, 16 | { pos: sourcePos, range: 1 }, 17 | { roomCallback: defaultRoomCallback({ ignoreFranchises: true }), } 18 | ); 19 | 20 | // failure cases 21 | const source = byId(sourceId); 22 | if (!source) { 23 | if (Game.rooms[sourcePos.roomName]) { 24 | return BehaviorResult.FAILURE; 25 | } else { 26 | return BehaviorResult.INPROGRESS; 27 | } 28 | } 29 | if (source.energy === 0) return BehaviorResult.FAILURE; 30 | 31 | // everything is in order: attempt to harvest 32 | if ( 33 | creep.harvest(source) === OK && 34 | creep.store.getFreeCapacity(RESOURCE_ENERGY) <= creep.getActiveBodyparts(WORK) * HARVEST_POWER 35 | ) { 36 | return BehaviorResult.SUCCESS; 37 | } 38 | return BehaviorResult.INPROGRESS; 39 | }; 40 | -------------------------------------------------------------------------------- /src/Behaviors/GetEnergy/fromStorageStructure.ts: -------------------------------------------------------------------------------- 1 | import { moveTo } from 'screeps-cartographer'; 2 | import { byId } from 'Selectors/byId'; 3 | import { BehaviorResult } from '../Behavior'; 4 | 5 | export const fromStorageStructure = (creep: Creep, structureId: Id) => { 6 | const structure = byId(structureId); 7 | if (!structure?.store.getUsedCapacity(RESOURCE_ENERGY)) return BehaviorResult.FAILURE; 8 | 9 | moveTo(creep, { pos: structure.pos, range: 1 }); 10 | if (creep.withdraw(structure, RESOURCE_ENERGY) === OK) { 11 | return BehaviorResult.SUCCESS; 12 | } 13 | return BehaviorResult.INPROGRESS; 14 | }; 15 | -------------------------------------------------------------------------------- /src/Behaviors/Labs/deposit.ts: -------------------------------------------------------------------------------- 1 | import { States } from 'Behaviors/states'; 2 | import { moveTo } from 'screeps-cartographer'; 3 | import { roomPlans } from 'Selectors/roomPlans'; 4 | import { LabOrder } from 'Structures/Labs/LabOrder'; 5 | 6 | export const deposit = 7 | (order?: LabOrder) => 8 | ({ office }: { office: string }, creep: Creep) => { 9 | const terminal = roomPlans(office)?.headquarters?.terminal.structure; 10 | if (!terminal) return States.DEPOSIT; 11 | 12 | if (creep.store.getUsedCapacity() === 0) { 13 | return States.WITHDRAW; 14 | } 15 | const toDeposit = Object.keys(creep.store)[0] as ResourceConstant | undefined; 16 | if (!toDeposit) { 17 | // Nothing further to deposit 18 | return States.WITHDRAW; 19 | } 20 | moveTo(creep, { pos: terminal.pos, range: 1 }); 21 | creep.transfer(terminal, toDeposit); 22 | if (order && toDeposit === order.output) { 23 | // Decrement output from the lab order 24 | order.amount -= creep.store.getUsedCapacity(toDeposit); 25 | } 26 | return States.DEPOSIT; 27 | }; 28 | -------------------------------------------------------------------------------- /src/Behaviors/Labs/emptyLabs.ts: -------------------------------------------------------------------------------- 1 | import { States } from 'Behaviors/states'; 2 | import { moveTo } from 'screeps-cartographer'; 3 | 4 | export const emptyLabs = ( 5 | { waitForReaction, labs }: { office: string; labs: StructureLab[]; waitForReaction?: boolean }, 6 | creep: Creep 7 | ) => { 8 | const threshold = waitForReaction ? 100 : 0; 9 | const target = labs.find(l => l.mineralType !== null && l.store.getUsedCapacity(l.mineralType) > threshold); 10 | const resource = target?.mineralType; 11 | if ((!waitForReaction && !resource) || creep.store.getFreeCapacity() === 0) { 12 | return States.DEPOSIT; 13 | } else if (!resource) { 14 | return States.EMPTY_LABS; // wait for reaction to collect more 15 | } 16 | moveTo(creep, { pos: target.pos, range: 1 }); 17 | creep.withdraw(target, resource); 18 | return States.EMPTY_LABS; 19 | }; 20 | -------------------------------------------------------------------------------- /src/Behaviors/Labs/fillLabs.ts: -------------------------------------------------------------------------------- 1 | import { States } from 'Behaviors/states'; 2 | import { moveTo } from 'screeps-cartographer'; 3 | import { LabMineralConstant } from 'Structures/Labs/LabOrder'; 4 | 5 | export const fillLabs = ( 6 | { fillOrders }: { fillOrders: [StructureLab, LabMineralConstant, number][] }, 7 | creep: Creep 8 | ) => { 9 | for (const [lab, resource, amount] of fillOrders) { 10 | const hasAmount = creep.store.getUsedCapacity(resource); 11 | if (hasAmount) { 12 | moveTo(creep, lab); 13 | creep.transfer(lab, resource, Math.min(amount, hasAmount, lab.store.getFreeCapacity(resource))); 14 | return States.FILL_LABS; 15 | } 16 | } 17 | return States.EMPTY_LABS; // nothing left to fill 18 | }; 19 | -------------------------------------------------------------------------------- /src/Behaviors/Labs/withdrawResources.ts: -------------------------------------------------------------------------------- 1 | import { States } from 'Behaviors/states'; 2 | import { moveTo } from 'screeps-cartographer'; 3 | import { roomPlans } from 'Selectors/roomPlans'; 4 | import { LabMineralConstant } from 'Structures/Labs/LabOrder'; 5 | 6 | export const withdrawResourcesFromTerminal = ( 7 | { office, withdrawResources }: { office: string; withdrawResources: [LabMineralConstant, number][] }, 8 | creep: Creep 9 | ) => { 10 | const terminal = roomPlans(office)?.headquarters?.terminal.structure; 11 | if (!terminal) return States.WITHDRAW; 12 | 13 | moveTo(creep, { pos: terminal.pos, range: 1 }); 14 | 15 | if (creep.pos.inRangeTo(terminal, 1)) { 16 | if (creep.store.getFreeCapacity() > 0) { 17 | for (const [resource, needed] of withdrawResources) { 18 | if (creep.store.getUsedCapacity(resource) >= needed || !terminal.store.getUsedCapacity(resource)) continue; 19 | // Need to get some of this resource 20 | creep.withdraw( 21 | terminal, 22 | resource, 23 | Math.min(needed, creep.store.getFreeCapacity(), terminal.store.getUsedCapacity(resource)) 24 | ); 25 | return States.WITHDRAW; 26 | } 27 | // No more resources to get 28 | } 29 | return States.FILL_LABS; 30 | } 31 | 32 | return States.WITHDRAW; 33 | }; 34 | -------------------------------------------------------------------------------- /src/Behaviors/blinkyKill.ts: -------------------------------------------------------------------------------- 1 | import { moveTo } from 'screeps-cartographer'; 2 | import { isAttacker } from 'Selectors/Combat/combatStats'; 3 | import { getRangeTo } from 'Selectors/Map/MapCoordinates'; 4 | 5 | export const blinkyKill = (creep: Creep, target?: Creep | Structure) => { 6 | const kite = target instanceof Creep && isAttacker(target); 7 | if (target) { 8 | if (kite && getRangeTo(creep.pos, target.pos) < 3) { 9 | moveTo(creep, { pos: target.pos, range: 3 }, { flee: true }); 10 | } else { 11 | moveTo(creep, { pos: target.pos, range: 1 }); 12 | } 13 | if (getRangeTo(creep.pos, target.pos) === 1 && !(target instanceof StructureWall)) { 14 | return creep.rangedMassAttack() === OK; 15 | } else { 16 | return creep.rangedAttack(target) === OK; 17 | } 18 | } 19 | return false; 20 | }; 21 | -------------------------------------------------------------------------------- /src/Behaviors/followPathHomeFromSource.ts: -------------------------------------------------------------------------------- 1 | import { franchiseRoadsToBuild } from 'Selectors/plannedFranchiseRoads'; 2 | import { roomPlans } from 'Selectors/roomPlans'; 3 | import { CachingStrategies, Keys, getCachedPath, moveByPath, moveTo } from 'screeps-cartographer'; 4 | import { BehaviorResult } from './Behavior'; 5 | 6 | export const followPathHomeFromSource = (creep: Creep, office: string, sourceId: Id) => { 7 | if (creep.pos.roomName === office) return BehaviorResult.SUCCESS; 8 | if (franchiseRoadsToBuild(office, sourceId).length > 10) { 9 | moveTo(creep, roomPlans(office)?.headquarters?.storage.pos ?? { pos: new RoomPosition(25, 25, office), range: 20 }); 10 | } else { 11 | moveByPath(creep, office + sourceId, { reverse: true }); 12 | } 13 | return BehaviorResult.INPROGRESS; 14 | }; 15 | 16 | export function isCloserToDestination(origin: Creep, target: Creep, office: string, sourceId?: Id) { 17 | // default to "go home" path 18 | let creepPath = getCachedPath(office + sourceId) 19 | ?.slice() 20 | .reverse(); 21 | if (!sourceId || origin.pos.roomName === office || franchiseRoadsToBuild(office, sourceId).length || !creepPath) { 22 | // use creep-cached path instead 23 | creepPath = getCachedPath(Keys.creepKey(origin, '_cp'), { cache: CachingStrategies.HeapCache }); 24 | } 25 | 26 | if (!creepPath) { 27 | return false; // no cached path found 28 | } 29 | 30 | const currentPos = creepPath.findIndex(t => t.isEqualTo(origin.pos)); 31 | 32 | if (currentPos === -1 || currentPos === creepPath.length - 1) { 33 | return false; // not on path, or at end of path 34 | } 35 | 36 | // if target is on the next step, it is closer 37 | if (creepPath[currentPos + 1].isEqualTo(target.pos)) { 38 | return true; 39 | } 40 | 41 | const targetSquare = creepPath[currentPos + 2]; 42 | 43 | if (targetSquare && target.pos.getRangeTo(targetSquare) < origin.pos.getRangeTo(targetSquare)) { 44 | // the other creep is closer to the destination along our current path 45 | return true; 46 | } 47 | // the other creep is not closer or it doesn't matter 48 | return false; 49 | } 50 | -------------------------------------------------------------------------------- /src/Behaviors/getEnergyFromFranchise.ts: -------------------------------------------------------------------------------- 1 | import { franchiseEnergyAvailable } from 'Selectors/Franchises/franchiseEnergyAvailable'; 2 | import { posById } from 'Selectors/posById'; 3 | import { resourcesNearPos } from 'Selectors/resourcesNearPos'; 4 | import { getFranchisePlanBySourceId } from 'Selectors/roomPlans'; 5 | import { moveByPath, moveTo } from 'screeps-cartographer'; 6 | import { BehaviorResult } from './Behavior'; 7 | 8 | export const getEnergyFromFranchise = (creep: Creep, office: string, franchise: Id) => { 9 | const pos = posById(franchise); 10 | if (!pos) return BehaviorResult.FAILURE; 11 | 12 | // TODO: This is vulnerable to edge cases where the creep needs to pick up a resource, but 13 | // the shortest path leads outside the room. 14 | if (creep.pos.roomName !== pos.roomName) { 15 | moveByPath(creep, office + franchise); 16 | return BehaviorResult.INPROGRESS; 17 | } 18 | 19 | if (franchiseEnergyAvailable(franchise) < 50 || creep.store.getFreeCapacity(RESOURCE_ENERGY) === 0) { 20 | return BehaviorResult.SUCCESS; 21 | } 22 | 23 | // First, pick up from container 24 | const container = getFranchisePlanBySourceId(franchise)?.container.structure as StructureContainer | undefined; 25 | if (container && container.store.getUsedCapacity(RESOURCE_ENERGY) > 0) { 26 | moveByPath(creep, office + franchise); 27 | creep.withdraw(container, RESOURCE_ENERGY); 28 | return BehaviorResult.INPROGRESS; 29 | } 30 | 31 | const resources = resourcesNearPos(pos, 1, RESOURCE_ENERGY); 32 | if (resources.length > 0) { 33 | // Otherwise, pick up loose resources 34 | const res = resources.shift(); 35 | if (res) { 36 | moveTo(creep, { pos: res.pos, range: 1 }); 37 | creep.pickup(res); 38 | } 39 | } 40 | 41 | return BehaviorResult.INPROGRESS; 42 | } 43 | -------------------------------------------------------------------------------- /src/Behaviors/getEnergyFromLink.ts: -------------------------------------------------------------------------------- 1 | import { moveTo } from 'screeps-cartographer'; 2 | import { roomPlans } from 'Selectors/roomPlans'; 3 | import profiler from 'utils/profiler'; 4 | import { BehaviorResult } from './Behavior'; 5 | 6 | export const getEnergyFromLink = profiler.registerFN((creep: Creep, office: string): BehaviorResult => { 7 | if (creep.store.getFreeCapacity(RESOURCE_ENERGY) === 0) return BehaviorResult.SUCCESS; 8 | 9 | const link = roomPlans(office)?.headquarters?.link.structure as StructureLink | undefined; 10 | if (!link || link.store.getUsedCapacity(RESOURCE_ENERGY) === 0) return BehaviorResult.FAILURE; 11 | 12 | moveTo(creep, { pos: link.pos, range: 1 }); 13 | if (creep.withdraw(link, RESOURCE_ENERGY) === OK) { 14 | return BehaviorResult.SUCCESS; 15 | } 16 | return BehaviorResult.INPROGRESS; 17 | }, 'getEnergyFromLink'); 18 | -------------------------------------------------------------------------------- /src/Behaviors/getEnergyFromRuin.ts: -------------------------------------------------------------------------------- 1 | import { moveTo } from 'screeps-cartographer'; 2 | import { byId } from 'Selectors/byId'; 3 | import profiler from 'utils/profiler'; 4 | import { BehaviorResult } from './Behavior'; 5 | 6 | declare global { 7 | interface CreepMemory { 8 | targetRuin?: Id; 9 | } 10 | } 11 | 12 | export const getEnergyFromRuin = profiler.registerFN((creep: Creep) => { 13 | // Default to specified franchise 14 | if (!creep.memory.targetRuin || byId(creep.memory.targetRuin)?.store.getUsedCapacity(RESOURCE_ENERGY) === 0) { 15 | // Select a new target: closest ruin with energy 16 | const target = creep.pos.findClosestByRange(FIND_RUINS, { 17 | filter: ruin => ruin.store.getUsedCapacity(RESOURCE_ENERGY) !== 0 18 | }); 19 | creep.memory.targetRuin = target?.id; 20 | } 21 | 22 | const ruin = byId(creep.memory.targetRuin); 23 | if (!ruin) return BehaviorResult.FAILURE; 24 | 25 | if (creep.store.getFreeCapacity(RESOURCE_ENERGY) === 0) { 26 | creep.memory.targetRuin = undefined; // Creep full 27 | return BehaviorResult.SUCCESS; 28 | } else { 29 | moveTo(creep, { pos: ruin.pos, range: 1 }); 30 | if (creep.pos.inRangeTo(ruin, 1)) { 31 | creep.withdraw(ruin, RESOURCE_ENERGY); 32 | } 33 | } 34 | 35 | return BehaviorResult.INPROGRESS; 36 | }, 'getEnergyFromRuin'); 37 | -------------------------------------------------------------------------------- /src/Behaviors/getEnergyFromStorageStructure.ts: -------------------------------------------------------------------------------- 1 | import { moveTo } from 'screeps-cartographer'; 2 | import { byId } from 'Selectors/byId'; 3 | import { storageEnergyAvailable } from 'Selectors/storageEnergyAvailable'; 4 | import { BehaviorResult } from './Behavior'; 5 | 6 | declare global { 7 | interface CreepMemory { 8 | targetStructure?: Id; 9 | } 10 | } 11 | 12 | export const getEnergyFromStorageStructure = (creep: Creep, office: string, limit?: number): BehaviorResult => { 13 | if (creep.store.getFreeCapacity(RESOURCE_ENERGY) === 0) return BehaviorResult.SUCCESS; 14 | 15 | const structure = byId(creep.memory.targetStructure); 16 | 17 | const withdrawLimit = 18 | structure instanceof StructureSpawn 19 | ? SPAWN_ENERGY_CAPACITY 20 | : limit ?? Game.rooms[office]?.energyCapacityAvailable ?? 0; 21 | 22 | if (!structure || !structure.store.getUsedCapacity(RESOURCE_ENERGY) || storageEnergyAvailable(office) < withdrawLimit) 23 | return BehaviorResult.FAILURE; 24 | 25 | moveTo(creep, structure); 26 | if (creep.withdraw(structure, RESOURCE_ENERGY) === OK) { 27 | return BehaviorResult.SUCCESS; 28 | } 29 | return BehaviorResult.INPROGRESS; 30 | }; 31 | -------------------------------------------------------------------------------- /src/Behaviors/getResourcesFromMineContainer.ts: -------------------------------------------------------------------------------- 1 | import { moveTo } from 'screeps-cartographer'; 2 | import { resourcesNearPos } from 'Selectors/resourcesNearPos'; 3 | import { roomPlans } from 'Selectors/roomPlans'; 4 | import profiler from 'utils/profiler'; 5 | import { BehaviorResult } from './Behavior'; 6 | 7 | export const getResourcesFromMineContainer = profiler.registerFN((creep: Creep, office: string) => { 8 | // Default to specified franchise 9 | const plan = roomPlans(office)?.mine; 10 | const container = plan?.container.structure as StructureContainer | undefined; 11 | if (!plan || !container) return BehaviorResult.FAILURE; 12 | 13 | if ( 14 | (resourcesNearPos(container.pos).length === 0 && container.store.getUsedCapacity() === 0) || 15 | creep.store.getFreeCapacity() === 0 16 | ) { 17 | return BehaviorResult.SUCCESS; // Mine container drained 18 | } else { 19 | // First, pick up from container 20 | const resourceType = Object.keys(container.store)[0] as ResourceConstant | undefined; 21 | if (resourceType && container.store.getUsedCapacity(resourceType) > 0) { 22 | moveTo(creep, { pos: container.pos, range: 1 }); 23 | creep.withdraw(container, resourceType); 24 | } else { 25 | // Otherwise, pick up loose resources 26 | const res = resourcesNearPos(plan.extractor.pos, 1).shift(); 27 | if (res) { 28 | moveTo(creep, { pos: res.pos, range: 1 }); 29 | creep.pickup(res); 30 | } 31 | } 32 | } 33 | 34 | return BehaviorResult.INPROGRESS; 35 | }, 'getResourcesFromMineContainer'); 36 | -------------------------------------------------------------------------------- /src/Behaviors/guardKill.ts: -------------------------------------------------------------------------------- 1 | import { moveTo } from 'screeps-cartographer'; 2 | 3 | export const guardKill = (creep: Creep, target?: Creep | Structure) => { 4 | if (target) { 5 | moveTo(creep, { pos: target.pos, range: 1 }); 6 | return creep.attack(target) === OK; 7 | } 8 | return false; 9 | }; 10 | -------------------------------------------------------------------------------- /src/Behaviors/harvestEnergyFromFranchise.ts: -------------------------------------------------------------------------------- 1 | import { defaultRoomCallback } from 'Selectors/Map/Pathing'; 2 | import { byId } from 'Selectors/byId'; 3 | import { posById } from 'Selectors/posById'; 4 | import { getFranchisePlanBySourceId } from 'Selectors/roomPlans'; 5 | import { adjacentWalkablePositions, move, moveTo } from 'screeps-cartographer'; 6 | import profiler from 'utils/profiler'; 7 | import { BehaviorResult } from './Behavior'; 8 | 9 | export const harvestEnergyFromFranchise = profiler.registerFN((creep: Creep, franchiseTarget: Id) => { 10 | const source = byId(franchiseTarget); 11 | const sourcePos = source?.pos ?? posById(franchiseTarget); 12 | const plan = getFranchisePlanBySourceId(franchiseTarget); 13 | 14 | if (!sourcePos || (Game.rooms[sourcePos.roomName] && !source)) { 15 | return BehaviorResult.FAILURE; 16 | } 17 | 18 | // Prefer to work from container position, fall back to adjacent position 19 | if ( 20 | plan && creep.pos.isEqualTo(plan.container.pos) 21 | ) { 22 | // stay here 23 | move(creep, [plan.container.pos], 3); 24 | } else if ( 25 | plan && 26 | (!Game.rooms[plan.container.pos.roomName] || 27 | plan.container.pos.lookFor(LOOK_CREEPS).filter(c => c.id !== creep.id).length === 0) 28 | ) { 29 | // go to container 30 | moveTo( 31 | creep, 32 | { pos: plan.container.pos, range: 0 }, 33 | { roomCallback: defaultRoomCallback({ ignoreFranchises: true }), priority: 3 } 34 | ); 35 | } else if ( 36 | (plan && creep.pos.inRangeTo(plan.container.pos, 1)) || 37 | adjacentWalkablePositions(sourcePos, false).length 38 | ) { 39 | // available squares to target 40 | moveTo(creep, sourcePos, { 41 | roomCallback: defaultRoomCallback({ ignoreFranchises: true }), 42 | priority: 3 43 | }); 44 | } else { 45 | // stand by in area 46 | moveTo(creep, { pos: sourcePos, range: 2 }); 47 | } 48 | 49 | if (creep.getActiveBodyparts(WORK) >= 10 && Game.time % 2 === 0) { 50 | return true; // harvest every other tick 51 | } 52 | return creep.harvest(source!) === OK; 53 | }, 'harvestEnergyFromFranchise'); 54 | -------------------------------------------------------------------------------- /src/Behaviors/recycle.ts: -------------------------------------------------------------------------------- 1 | import { roomPlans } from 'Selectors/roomPlans'; 2 | import { moveTo } from 'screeps-cartographer'; 3 | import { States } from './states'; 4 | 5 | export const recycle = ( 6 | data: { 7 | office: string; 8 | }, 9 | creep: Creep 10 | ): States.RECYCLE => { 11 | const recycleTarget = roomPlans(data.office)?.fastfiller?.containers[0].pos; 12 | const recycleSpawn = roomPlans(data.office)?.fastfiller?.spawns[0].structure as StructureSpawn | undefined; 13 | if (!recycleTarget || !recycleSpawn) { 14 | // oh well, we tried 15 | creep.suicide(); 16 | return States.RECYCLE; 17 | } 18 | moveTo(creep, { pos: recycleTarget, range: 0 }); 19 | if (creep.pos.isEqualTo(recycleTarget)) recycleSpawn.recycleCreep(creep); 20 | return States.RECYCLE; 21 | }; 22 | -------------------------------------------------------------------------------- /src/Behaviors/signRoom.ts: -------------------------------------------------------------------------------- 1 | import { moveTo } from 'screeps-cartographer'; 2 | import { controllerPosition } from 'Selectors/roomCache'; 3 | import { BehaviorResult } from './Behavior'; 4 | 5 | export function signRoom(creep: Creep, room: string) { 6 | const controllerPos = controllerPosition(room); 7 | if (!controllerPos) return BehaviorResult.FAILURE; 8 | moveTo(creep, { pos: controllerPos, range: 1 }); 9 | if (!Game.rooms[room]) return BehaviorResult.INPROGRESS; 10 | const controller = Game.rooms[room].controller; 11 | if (!controller) return BehaviorResult.FAILURE; 12 | creep.signController(controller, 'This sector property of the Grey Company'); 13 | return BehaviorResult.SUCCESS; 14 | } 15 | -------------------------------------------------------------------------------- /src/Behaviors/stateMachine.ts: -------------------------------------------------------------------------------- 1 | import { logCpu, logCpuStart } from "utils/logCPU"; 2 | 3 | declare global { 4 | interface CreepMemory { 5 | runState?: string; 6 | } 7 | } 8 | 9 | export function runStates( 10 | states: Record States>, 11 | data: M, 12 | creep: Creep, 13 | debug: {cpu?: boolean, states?: boolean} = {} 14 | ) { 15 | const statesRun: string[] = []; 16 | let state = (creep.memory.runState ?? Object.keys(states)[0]) as States | ExtraStates; // First state is default 17 | creep.memory.runState = state; 18 | if (debug.states) console.log(creep.name, 'starting at', state); 19 | if (debug.cpu) logCpuStart() 20 | while (!statesRun.includes(state)) { 21 | statesRun.push(state); 22 | if (!(state in states)) { 23 | delete creep.memory.runState; 24 | throw new Error(`Mission has no state: ${state}`); 25 | } 26 | state = states[state](data, creep); 27 | if (debug.states) console.log(creep.name, 'switching to', state); 28 | if (debug.cpu) logCpu(state) 29 | creep.memory.runState = state; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Behaviors/states.ts: -------------------------------------------------------------------------------- 1 | export enum States { 2 | GET_ENERGY = 'GET_ENERGY', 3 | GET_ENERGY_FRANCHISE = 'GET_ENERGY_FRANCHISE', 4 | GET_ENERGY_STORAGE = 'GET_ENERGY_STORAGE', 5 | GET_ENERGY_LINK = 'GET_ENERGY_LINK', 6 | GET_ENERGY_SOURCE = 'GET_ENERGY_SOURCE', 7 | GET_ENERGY_RUINS = 'GET_ENERGY_RUINS', 8 | WORKING = 'WORKING', 9 | FIND_WITHDRAW = 'FIND_WITHDRAW', 10 | WITHDRAW = 'WITHDRAW', 11 | FIND_DEPOSIT = 'FIND_DEPOSIT', 12 | DEPOSIT = 'DEPOSIT', 13 | DONE = 'DONE', 14 | FILL_TOWERS = 'FILL_TOWERS', 15 | FILL_LEGAL = 'FILL_LEGAL', 16 | FILL_LABS = 'FILL_LABS', 17 | EMPTY_LABS = 'EMPTY_LABS', 18 | RENEW = 'RENEW', 19 | GET_BOOSTED = 'GET_BOOSTED', 20 | RECYCLE = 'RECYCLE', 21 | FIND_WORK = 'FIND_WORK', 22 | BUILDING = 'BUILDING', 23 | UPGRADING = 'UPGRADING', 24 | DEFEND = 'DEFEND' 25 | } 26 | -------------------------------------------------------------------------------- /src/DEBUG_reset_Memory.js: -------------------------------------------------------------------------------- 1 | // To clean up after a private server restore 2 | 3 | for (let key in Memory) { 4 | delete Memory[key]; 5 | } 6 | -------------------------------------------------------------------------------- /src/Intel/Franchises/updateFranchisePaths.ts: -------------------------------------------------------------------------------- 1 | import { ScannedFranchiseEvent } from 'Intel/events'; 2 | import { planFranchisePath } from 'Selectors/Franchises/planFranchisePath'; 3 | 4 | export const updateFranchisePaths = ({ office, source }: ScannedFranchiseEvent) => { 5 | planFranchisePath(office, source); 6 | }; 7 | -------------------------------------------------------------------------------- /src/Intel/Franchises/updateLedger.ts: -------------------------------------------------------------------------------- 1 | import { FRANCHISE_EVALUATE_PERIOD, FRANCHISE_RETRY_INTERVAL } from 'config'; 2 | import { ScannedFranchiseEvent } from 'Intel/events'; 3 | import { HarvestLedger } from 'Ledger/HarvestLedger'; 4 | import { franchiseActive } from 'Selectors/Franchises/franchiseActive'; 5 | 6 | export const updateLedger = ({ office, source, remote }: ScannedFranchiseEvent) => { 7 | if (!remote) return; 8 | const ledger = HarvestLedger.get(office, source); 9 | 10 | if (ledger.age < 1500 || !Memory.offices[office].franchises[source]) return; 11 | 12 | Memory.offices[office].franchises[source].scores ??= []; 13 | const { scores, lastActive: lastHarvested } = Memory.offices[office].franchises[source]; 14 | 15 | if (franchiseActive(office, source)) { 16 | // record score for previous 1500 ticks 17 | scores.push(ledger.perTick); 18 | if (scores.length > FRANCHISE_EVALUATE_PERIOD) scores.shift(); 19 | 20 | // console.log(office, room, source, JSON.stringify(ledger.value), scores); 21 | } else { 22 | // unprofitable franchise was abandoned - evaluate if scores should be reset 23 | if ( 24 | scores.length === FRANCHISE_EVALUATE_PERIOD && 25 | scores.reduce((a, b) => a + b, 0) / scores.length <= 1 && 26 | lastHarvested && 27 | lastHarvested < Game.time - FRANCHISE_RETRY_INTERVAL 28 | ) { 29 | // franchise was producing less than 1 e/t, but it's time to re-evaluate 30 | scores.splice(0, FRANCHISE_EVALUATE_PERIOD); 31 | } 32 | } 33 | 34 | HarvestLedger.reset(office, source); 35 | }; 36 | -------------------------------------------------------------------------------- /src/Intel/Rooms/initializeOfficeMemory.ts: -------------------------------------------------------------------------------- 1 | import { ScannedRoomEvent } from 'Intel/events'; 2 | import { destroyUnplannedStructures } from 'Selectors/Structures/destroyUnplannedStructures'; 3 | import { cityNames } from 'utils/CityNames'; 4 | 5 | export const initializeOfficeMemory = ({ room, office }: ScannedRoomEvent) => { 6 | if (!office || Memory.offices[room]) return; 7 | 8 | // Initialize new office 9 | Memory.offices[room] = { 10 | city: cityNames.find(name => !Object.values(Memory.offices).some(r => r.city === name)) ?? room, 11 | resourceQuotas: { 12 | [RESOURCE_ENERGY]: 10000 13 | }, 14 | lab: { 15 | orders: [], 16 | boosts: [], 17 | boostingLabs: [] 18 | }, 19 | powerbanks: [], 20 | franchises: {} 21 | }; 22 | 23 | Memory.rooms[room].rclMilestones = {}; 24 | Memory.rooms[room].rclMilestones![Game.rooms[room].controller!.level] ??= Game.time; 25 | 26 | destroyUnplannedStructures(room); 27 | }; 28 | -------------------------------------------------------------------------------- /src/Intel/Rooms/mineralQuotas.ts: -------------------------------------------------------------------------------- 1 | import { MINERALS } from 'gameConstants'; 2 | import { ScannedRoomEvent } from 'Intel/events'; 3 | import { ownedMinerals } from 'Selectors/ownedMinerals'; 4 | 5 | export const mineralQuotas = ({ room, office }: ScannedRoomEvent) => { 6 | if (!office) return; 7 | // Temporary quotas for minerals 8 | for (let m of ownedMinerals()) { 9 | Memory.offices[room].resourceQuotas[m as ResourceConstant] = 3000; 10 | } 11 | for (let o of Memory.offices[room].lab.orders) { 12 | if (MINERALS.includes(o.ingredient1)) { 13 | Memory.offices[room].resourceQuotas[o.ingredient1] = 3000; 14 | } 15 | if (MINERALS.includes(o.ingredient2)) { 16 | Memory.offices[room].resourceQuotas[o.ingredient2] = 3000; 17 | } 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/Intel/Rooms/purgeDeadOffices.ts: -------------------------------------------------------------------------------- 1 | export const purgeDeadOffices = () => { 2 | for (let office in Memory.offices) { 3 | // if (rcl(office) > 1 && !Game.rooms[office]?.find(FIND_MY_SPAWNS).length && Object.keys(Memory.offices).length > 1) { 4 | // // Office was destroyed 5 | // Game.rooms[office]?.controller?.unclaim(); 6 | // } 7 | if (!Game.rooms[office]?.controller?.my) { 8 | delete Memory.offices[office]; 9 | delete Memory.stats.offices[office]; 10 | } 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/Intel/events.ts: -------------------------------------------------------------------------------- 1 | export type ScannedRoomEvent = { room: string; office: boolean; territory: boolean }; 2 | export type ScannedFranchiseEvent = { office: string; source: Id; remote: boolean }; 3 | -------------------------------------------------------------------------------- /src/Ledger/AccuracyLedger.ts: -------------------------------------------------------------------------------- 1 | import { DetailLedger } from 'Ledger'; 2 | 3 | export class AccuracyLedger { 4 | static Ledger = new DetailLedger(); 5 | static id(office: string) { 6 | return `a_${office}`; 7 | } 8 | static reset(office: string) { 9 | return this.Ledger.reset(this.id(office)); 10 | } 11 | static get(office: string) { 12 | return this.Ledger.get(this.id(office)); 13 | } 14 | static record(office: string, label: string, value: number) { 15 | return this.Ledger.record(this.id(office), label, value); 16 | } 17 | } 18 | 19 | export function reportAccuracyLedger() { 20 | console.log('\nAccuracy Ledger\n'); 21 | for (const room in Memory.dledger) { 22 | if (!room.startsWith('a_')) continue; 23 | // AccuracyLedger.reset(room.replace('a_', '')); 24 | const ledger = Memory.dledger[room]; 25 | console.log( 26 | room, 27 | Game.time - ledger.created, 28 | (-(ledger.value.transfer ?? 0) + -(ledger.value.drop ?? 0) + -(ledger.value.used ?? 0)) / 29 | (ledger.value.harvest ?? 0), 30 | JSON.stringify(ledger.value) 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Ledger/AcquireLedger.ts: -------------------------------------------------------------------------------- 1 | import { Ledger } from 'Ledger'; 2 | 3 | export class AcquireLedger { 4 | static Ledger = new Ledger(CREEP_CLAIM_LIFE_TIME, CREEP_CLAIM_LIFE_TIME * 10); 5 | static id(office: string, source: Id) { 6 | return `r_${office}${source}`; 7 | } 8 | static get(office: string, sourceId: Id) { 9 | return this.Ledger.get(this.id(office, sourceId)); 10 | } 11 | static record(office: string, sourceId: Id, value: number) { 12 | return this.Ledger.record(this.id(office, sourceId), value); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Ledger/HarvestLedger.ts: -------------------------------------------------------------------------------- 1 | import { DetailLedger } from 'Ledger'; 2 | import { franchisesByOffice } from 'Selectors/Franchises/franchisesByOffice'; 3 | 4 | export class HarvestLedger { 5 | static Ledger = new DetailLedger(); 6 | static id(office: string, source: Id) { 7 | return `r_${office}${source}`; 8 | } 9 | static reset(office: string, sourceId: Id) { 10 | return this.Ledger.reset(this.id(office, sourceId)); 11 | } 12 | static get(office: string, sourceId: Id) { 13 | return this.Ledger.get(this.id(office, sourceId)); 14 | } 15 | static record(office: string, sourceId: Id, label: string, value: number) { 16 | return this.Ledger.record(this.id(office, sourceId), label, value); 17 | } 18 | } 19 | 20 | export function reportHarvestLedger() { 21 | console.log('\nHarvest Ledger\n'); 22 | for (const office in Memory.offices) { 23 | for (const { source, room } of franchisesByOffice(office)) { 24 | // HarvestLedger.reset(office); 25 | const ledger = HarvestLedger.get(office, source); 26 | console.log(office, room, ledger.age, JSON.stringify(ledger.value)); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Ledger/LogisticsLedger.ts: -------------------------------------------------------------------------------- 1 | import { DetailLedger } from 'Ledger'; 2 | 3 | export class LogisticsLedger { 4 | static Ledger = new DetailLedger(); 5 | static id(office: string) { 6 | return `l_${office}`; 7 | } 8 | static reset(office: string) { 9 | return this.Ledger.reset(this.id(office)); 10 | } 11 | static get(office: string) { 12 | return this.Ledger.get(this.id(office)); 13 | } 14 | static record(office: string, label: string, value: number) { 15 | return this.Ledger.record(this.id(office), label, value); 16 | } 17 | } 18 | 19 | export function reportLogisticsLedger() { 20 | console.log('\nLogistics Ledger\n'); 21 | for (const office in Memory.offices) { 22 | // LogisticsLedger.reset(office); 23 | const ledger = LogisticsLedger.get(office); 24 | const outputs = (ledger.value.deposit ?? 0) + (ledger.value.decay ?? 0) + (ledger.value.death ?? 0); 25 | const inputs = (ledger.value.recover ?? 0) + (ledger.value.harvest ?? 0); 26 | const logisticsEfficiency = inputs ? -outputs / inputs : 0; 27 | console.log(office, ledger.age, logisticsEfficiency, JSON.stringify(ledger.value)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Metrics/heapMetrics.ts: -------------------------------------------------------------------------------- 1 | import { Metrics } from 'screeps-viz'; 2 | 3 | interface HeapMetrics { 4 | roomEnergy: Metrics.Timeseries; 5 | spawnEfficiency: Metrics.Timeseries; 6 | } 7 | 8 | export const heapMetrics: Record = {}; 9 | -------------------------------------------------------------------------------- /src/Minions/Builds/accountant.ts: -------------------------------------------------------------------------------- 1 | import { memoize } from 'utils/memoizeFunction'; 2 | import { buildFromSegment, CreepBuild, unboosted } from './utils'; 3 | 4 | export const buildAccountant = memoize( 5 | // Memoizes at 50-energy increments 6 | (energy: number, maxSegments = 25, roads = false, repair = false) => 7 | `${Math.round((energy * 2) / 100)} ${maxSegments} ${roads}`, 8 | (energy: number, maxSegments = 25, roads = false, repair = false): CreepBuild[] => { 9 | const suffix = energy < 350 ? [] : repair ? (roads ? [WORK, CARRY, MOVE] : [WORK, MOVE]) : []; 10 | if (energy < 100 || maxSegments === 0) { 11 | return []; 12 | } else if (energy < 5600) { 13 | // Before we have two spawns, create smaller haulers 14 | if (!roads) { 15 | return [unboosted(buildFromSegment(energy, [CARRY, MOVE], { maxSegments: Math.min(maxSegments, 13), suffix }))]; 16 | } else { 17 | return [unboosted( 18 | buildFromSegment(energy, [CARRY, CARRY, MOVE], { maxSegments: Math.min(maxSegments, 13), suffix }) 19 | )]; 20 | } 21 | } else { 22 | if (!roads) { 23 | return [unboosted(buildFromSegment(energy, [CARRY, MOVE], { maxSegments, suffix }))]; 24 | } else { 25 | return [unboosted(buildFromSegment(energy, [CARRY, CARRY, MOVE], { maxSegments, suffix }))]; 26 | } 27 | } 28 | } 29 | ); 30 | -------------------------------------------------------------------------------- /src/Minions/Builds/auditor.ts: -------------------------------------------------------------------------------- 1 | import { CreepBuild, unboosted } from './utils'; 2 | 3 | export const buildAuditor = (): CreepBuild[] => { 4 | return [unboosted([MOVE])]; 5 | }; 6 | -------------------------------------------------------------------------------- /src/Minions/Builds/clerk.ts: -------------------------------------------------------------------------------- 1 | import { buildFromSegment, CreepBuild, unboosted } from './utils'; 2 | 3 | export const buildClerk = (energy: number, maxSegments = 50, mobile = false): CreepBuild[] => { 4 | return [unboosted(buildFromSegment(energy, [CARRY], { maxSegments, suffix: mobile ? [MOVE] : [] }))]; 5 | }; 6 | -------------------------------------------------------------------------------- /src/Minions/Builds/dismantler.ts: -------------------------------------------------------------------------------- 1 | import { BOOSTS_BY_INTENT } from 'gameConstants'; 2 | import { buildCost } from 'Selectors/minionCostPerTick'; 3 | import { buildFromSegment, CreepBuild } from './utils'; 4 | 5 | export const buildDismantler = (energy: number, maxTier: 0 | 1 | 2 | 3 = 3): CreepBuild[] => { 6 | const tiers = [3, 2, 1, 0].filter(tier => tier <= maxTier); 7 | if (energy < 150) { 8 | return []; 9 | } else { 10 | // Maintain 1-1 WORK-MOVE ratio 11 | const body = buildFromSegment(energy, [WORK, MOVE]); 12 | const count = body.filter(p => p === WORK).length; 13 | // boost, when available 14 | return tiers.map(tier => { 15 | const boosts = tier > 0 ? [{ type: BOOSTS_BY_INTENT.DISMANTLE[tier - 1], count }] : []; 16 | return { 17 | body, 18 | boosts, 19 | tier, 20 | cost: buildCost(body, boosts) 21 | }; 22 | }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/Minions/Builds/engineer.ts: -------------------------------------------------------------------------------- 1 | import { buildFromSegment, CreepBuild, unboosted } from './utils'; 2 | 3 | export const buildEngineer = (energy: number, roads = false, near = false): CreepBuild[] => { 4 | if (near) { 5 | if (roads) { 6 | return [unboosted(buildFromSegment(energy, [WORK, MOVE, CARRY, CARRY]))]; 7 | } else { 8 | return [unboosted(buildFromSegment(energy, [WORK, MOVE, MOVE, CARRY, CARRY]))]; 9 | } 10 | } else { 11 | if (roads) { 12 | if (energy <= 500) return [unboosted(buildFromSegment(energy, [WORK, MOVE, CARRY, CARRY]))]; 13 | return [unboosted(buildFromSegment(energy, [WORK, MOVE, MOVE, CARRY, CARRY, CARRY, CARRY, CARRY, CARRY]))]; 14 | } else { 15 | if (energy <= 550) return [unboosted(buildFromSegment(energy, [WORK, MOVE, MOVE, CARRY, CARRY]))]; 16 | if (energy <= 1800) 17 | return [unboosted( 18 | buildFromSegment(energy, [WORK, MOVE, MOVE, MOVE, MOVE, CARRY, CARRY, CARRY, CARRY, CARRY, CARRY]) 19 | )]; 20 | // prettier-ignore 21 | return [unboosted([ 22 | WORK, WORK, WORK, 23 | MOVE, MOVE, MOVE, MOVE, MOVE, MOVE, MOVE, MOVE, MOVE, MOVE, MOVE, MOVE, MOVE, 24 | CARRY, CARRY, CARRY, CARRY, CARRY, CARRY, CARRY, CARRY, CARRY, CARRY, 25 | CARRY, CARRY, CARRY, CARRY, CARRY, CARRY, CARRY, CARRY, CARRY, CARRY 26 | ])] 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/Minions/Builds/foreman.ts: -------------------------------------------------------------------------------- 1 | import { BOOSTS_BY_INTENT } from 'gameConstants'; 2 | import { buildCost } from 'Selectors/minionCostPerTick'; 3 | import { buildFromSegment, CreepBuild } from './utils'; 4 | 5 | export const buildForeman = (energy: number, maxTier: 0 | 1 | 2 | 3 = 3): CreepBuild[] => { 6 | const tiers = [3, 2, 1, 0].filter(tier => tier <= maxTier); 7 | if (energy < 550) { 8 | return []; 9 | } else { 10 | // Maintain 4-1 WORK-MOVE ratio 11 | const body = buildFromSegment(energy, [WORK, WORK, WORK, WORK, MOVE]); 12 | const count = body.filter(p => p === WORK).length; 13 | // boost, when available 14 | return tiers.map(tier => { 15 | const boosts = tier > 0 ? [{ type: BOOSTS_BY_INTENT.HARVEST[tier - 1], count }] : []; 16 | return { 17 | body, 18 | boosts, 19 | tier, 20 | cost: buildCost(body, boosts) 21 | }; 22 | }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/Minions/Builds/guard.ts: -------------------------------------------------------------------------------- 1 | import { buildFromSegment, CreepBuild, unboosted } from './utils'; 2 | 3 | export const buildGuard = (energy: number, heal = false): CreepBuild[] => { 4 | if (energy < 200) { 5 | return []; 6 | } else if (heal && energy >= 420) { 7 | // Add a heal part 8 | return [unboosted(buildFromSegment(energy, [ATTACK, MOVE], { sorted: true, suffix: [HEAL, MOVE] }))]; 9 | } else { 10 | return [unboosted(buildFromSegment(energy, [ATTACK, MOVE], { sorted: true }))]; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/Minions/Builds/lawyer.ts: -------------------------------------------------------------------------------- 1 | import { CreepBuild, unboosted } from './utils'; 2 | 3 | export const buildLawyer = (energy: number): CreepBuild[] => { 4 | if (energy < 850) { 5 | return []; 6 | } else { 7 | return [unboosted([CLAIM, MOVE, MOVE, MOVE, MOVE, MOVE])]; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/Minions/Builds/marketer.ts: -------------------------------------------------------------------------------- 1 | import { buildFromSegment, CreepBuild, unboosted } from './utils'; 2 | 3 | export const buildMarketer = (energy: number): CreepBuild[] => { 4 | if (energy < 650) { 5 | return []; 6 | } else { 7 | return [unboosted(buildFromSegment(energy, [CLAIM, MOVE], { maxSegments: 5 }))]; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/Minions/Builds/medic.ts: -------------------------------------------------------------------------------- 1 | import { buildFromSegment, CreepBuild, unboosted } from './utils'; 2 | 3 | export const buildMedic = (energy: number): CreepBuild[] => { 4 | if (energy < 200) { 5 | return []; 6 | } else { 7 | return [unboosted(buildFromSegment(energy, [HEAL, MOVE], { sorted: true }))]; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/Minions/Builds/powerbank.ts: -------------------------------------------------------------------------------- 1 | import { BOOSTS_BY_INTENT } from 'gameConstants'; 2 | import { buildCost } from 'Selectors/minionCostPerTick'; 3 | import { CreepBuild } from './utils'; 4 | 5 | export const buildPowerbankAttacker = (): CreepBuild[] => { 6 | const builds: CreepBuild[] = []; 7 | const tiers = [ 8 | { tough: 2, attack: 38, move: 10, tier: 3 }, 9 | { tough: 3, attack: 34, move: 13, tier: 2 }, 10 | { tough: 3, attack: 30, move: 17, tier: 1 }, 11 | { tough: 0, attack: 22, move: 28, tier: 0 } 12 | ]; 13 | for (const { tough, attack, move, tier } of tiers) { 14 | const body = ([] as BodyPartConstant[]).concat( 15 | Array(tough).fill(TOUGH), 16 | Array(move).fill(MOVE), 17 | Array(attack).fill(ATTACK) 18 | ); 19 | const boosts = tier 20 | ? [ 21 | { type: BOOSTS_BY_INTENT.TOUGH[tier - 1], count: tough }, 22 | { type: BOOSTS_BY_INTENT.ATTACK[tier - 1], count: attack }, 23 | { type: BOOSTS_BY_INTENT.MOVE[tier - 1], count: move } 24 | ] 25 | : []; 26 | // 27 | builds.push({ 28 | body, 29 | boosts, 30 | tier, 31 | cost: buildCost(body, boosts) 32 | }); 33 | } 34 | return builds; 35 | }; 36 | 37 | export const buildPowerbankHealer = (): CreepBuild[] => { 38 | const builds: CreepBuild[] = []; 39 | const tiers = [ 40 | { heal: 38, move: 10, tier: 3 }, 41 | { heal: 35, move: 11, tier: 2 }, 42 | { heal: 33, move: 16, tier: 1 }, 43 | { heal: 28, move: 22, tier: 0 } 44 | ]; 45 | for (const { heal, move, tier } of tiers) { 46 | const body = ([] as BodyPartConstant[]).concat(Array(move).fill(MOVE), Array(heal).fill(HEAL)); 47 | const boosts = tier 48 | ? [ 49 | { type: BOOSTS_BY_INTENT.HEAL[tier - 1], count: heal }, 50 | { type: BOOSTS_BY_INTENT.MOVE[tier - 1], count: move } 51 | ] 52 | : []; 53 | builds.push({ 54 | body, 55 | boosts, 56 | tier, 57 | cost: buildCost(body, boosts) 58 | }); 59 | } 60 | return builds; 61 | }; 62 | -------------------------------------------------------------------------------- /src/Minions/Builds/research.ts: -------------------------------------------------------------------------------- 1 | import { BOOSTS_BY_INTENT } from 'gameConstants'; 2 | import { buildCost } from 'Selectors/minionCostPerTick'; 3 | import { CreepBuild } from './utils'; 4 | 5 | export const buildResearch = (energy: number, maxWorkParts = 15): CreepBuild[] => { 6 | if (energy < 250 || maxWorkParts <= 0) { 7 | return []; 8 | } else { 9 | // Max for an upgrader at RCL8 is 15 energy/tick, so we'll cap these there 10 | let workParts = Math.max(1, Math.min(Math.floor(maxWorkParts), Math.floor((energy * 10) / 13 / 100))); 11 | let carryParts = Math.max(1, Math.min(3, Math.floor((energy * 1) / 13 / 50))); 12 | let moveParts = Math.max(1, Math.min(6, Math.floor((energy * 2) / 13 / 50))); 13 | // console.log(energy, maxWorkParts, workParts) 14 | const body = ([] as BodyPartConstant[]).concat( 15 | Array(workParts).fill(WORK), 16 | Array(carryParts).fill(CARRY), 17 | Array(moveParts).fill(MOVE) 18 | ); 19 | 20 | const tiers = [3, 2, 1, 0]; 21 | 22 | // any level of boosts, depending on availability 23 | return tiers.map(tier => { 24 | const boosts = tier > 0 ? [{ type: BOOSTS_BY_INTENT.UPGRADE[tier - 1], count: workParts }] : []; 25 | return { body, boosts, tier, cost: buildCost(body, boosts) }; 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/Minions/Builds/salesman.ts: -------------------------------------------------------------------------------- 1 | import { buildFromSegment, CreepBuild, unboosted } from './utils'; 2 | 3 | export const buildSalesman = (energy: number, link = false, remote = false): CreepBuild[] => { 4 | if (energy < 200) { 5 | return []; 6 | } else if (energy < 550) { 7 | return [unboosted([WORK, WORK, MOVE])]; 8 | } else if (energy < 600) { 9 | return link || remote 10 | ? [unboosted([WORK, WORK, WORK, CARRY, MOVE])] 11 | : [unboosted([WORK, WORK, WORK, WORK, WORK, MOVE])]; 12 | } 13 | 14 | if (remote) { 15 | return [unboosted(buildFromSegment(energy, [WORK, WORK, WORK, MOVE], { maxSegments: 2, suffix: [CARRY] }))]; 16 | } else { // costs 600 with link 17 | return [unboosted( 18 | buildFromSegment(energy, [WORK, WORK, WORK, WORK, WORK, MOVE], { 19 | maxSegments: 2, 20 | suffix: link ? [CARRY] : [] 21 | }) 22 | )]; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/Minions/minionTypes.ts: -------------------------------------------------------------------------------- 1 | export enum MinionTypes { 2 | ACCOUNTANT = 'ACCOUNTANT', 3 | CLERK = 'CLERK', 4 | ENGINEER = 'ENGINEER', 5 | PAVER = 'PAVER', 6 | FOREMAN = 'FOREMAN', 7 | GUARD = 'GUARD', 8 | AUDITOR = 'AUDITOR', 9 | LAWYER = 'LAWYER', 10 | RESEARCH = 'PARALEGAL', 11 | SALESMAN = 'SALESMAN', 12 | MARKETER = 'MARKETER', 13 | BLINKY = 'BLINKY', 14 | MEDIC = 'MEDIC', 15 | POWER_BANK_ATTACKER = 'PBA', 16 | POWER_BANK_HEALER = 'PBH', 17 | DISMANTLER = 'DISMANTLER' 18 | } 19 | -------------------------------------------------------------------------------- /src/Minions/runSpawns.ts: -------------------------------------------------------------------------------- 1 | import { adjacentWalkablePositions } from "Selectors/Map/MapCoordinates"; 2 | import { getSpawns, roomPlans } from "Selectors/roomPlans"; 3 | import profiler from "utils/profiler"; 4 | 5 | export const runSpawns = profiler.registerFN((office: string) => { 6 | const spawns = getSpawns(office); 7 | if (spawns.length === 0) { 8 | // place initial spawn site 9 | roomPlans(office)?.headquarters?.spawn.pos.createConstructionSite(STRUCTURE_SPAWN); 10 | } 11 | spawns.forEach(s => { 12 | if ( 13 | s.spawning?.directions && 14 | !adjacentWalkablePositions(s.pos) // One of the spawning directions is walkable 15 | .some(pos => s.spawning!.directions.includes(s.pos.getDirectionTo(pos))) 16 | ) { 17 | if (s.spawning.remainingTime < 2) { 18 | s.pos.findInRange(FIND_MY_CREEPS, 1).forEach(c => c.move(s.pos.getDirectionTo(c.pos.x, c.pos.y))); 19 | } 20 | } 21 | }) 22 | }, 'runSpawns') 23 | -------------------------------------------------------------------------------- /src/Missions/BaseClasses/CreepSpawner/ConditionalCreepSpawner.ts: -------------------------------------------------------------------------------- 1 | import { CPU_ESTIMATE_PERIOD } from 'config'; 2 | import { BaseCreepSpawner } from './BaseCreepSpawner'; 3 | 4 | declare global { 5 | interface CreepMemory { 6 | arrived?: number; 7 | } 8 | } 9 | 10 | export class ConditionalCreepSpawner extends BaseCreepSpawner { 11 | constructor(id: string, office: string, public props: BaseCreepSpawner['props'] & { shouldSpawn?: () => boolean }) { 12 | super(id, office, props); 13 | } 14 | 15 | spawn(missionId: CreepMemory['missionId'], priority: number) { 16 | if (this.resolved || !this.props.shouldSpawn?.()) return []; 17 | return super.spawn(missionId, priority); 18 | } 19 | 20 | public _creep?: string; 21 | 22 | get resolved(): Creep | undefined { 23 | return this._creep ? Game.creeps[this._creep] : undefined; 24 | } 25 | 26 | get spawned() { 27 | return Boolean(this.memory?.spawned); 28 | } 29 | 30 | register(creep: Creep, onNew?: () => void) { 31 | if (this.memory) { 32 | this.memory.spawned = true; 33 | this.checkOnSpawn(creep, onNew); 34 | } 35 | this._creep = creep.name; 36 | } 37 | 38 | get died() { 39 | return this.memory?.spawned && (!this._creep || !Game.creeps[this._creep]); 40 | } 41 | 42 | cpuRemaining(): number { 43 | return ( 44 | Math.min(CPU_ESTIMATE_PERIOD, this.resolved?.ticksToLive ?? Infinity) * this.cpuPerTick 45 | ); 46 | } 47 | 48 | ttlRemaining(): number { 49 | return Math.min(CPU_ESTIMATE_PERIOD, this.resolved?.ticksToLive ?? Infinity) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Missions/BaseClasses/CreepSpawner/CreepSpawner.ts: -------------------------------------------------------------------------------- 1 | import { prespawnByArrived } from 'Selectors/prespawn'; 2 | import { CPU_ESTIMATE_PERIOD } from 'config'; 3 | import { BaseCreepSpawner } from './BaseCreepSpawner'; 4 | 5 | declare global { 6 | interface CreepMemory { 7 | arrived?: number; 8 | } 9 | } 10 | 11 | export class CreepSpawner extends BaseCreepSpawner { 12 | constructor( 13 | id: string, 14 | office: string, 15 | public props: BaseCreepSpawner['props'] & { prespawn?: boolean; respawn?: () => boolean }, 16 | public eventHandlers?: BaseCreepSpawner['eventHandlers'] 17 | ) { 18 | super(id, office, props, eventHandlers); 19 | } 20 | 21 | spawn(missionId: CreepMemory['missionId'], priority: number) { 22 | const prespawn = this.props.prespawn && this.resolved && prespawnByArrived(this.resolved); 23 | if (this.memory?.spawned && !this.props.respawn?.()) return []; 24 | if (this.resolved && !prespawn) return []; 25 | return super.spawn(missionId, priority); 26 | } 27 | 28 | public _creep?: string; 29 | 30 | get resolved(): Creep | undefined { 31 | return this._creep ? Game.creeps[this._creep] : undefined; 32 | } 33 | 34 | get spawned() { 35 | return Boolean(this.memory?.spawned); 36 | } 37 | 38 | register(creep: Creep, onNew?: () => void) { 39 | if (this.memory) { 40 | this.memory.spawned = true; 41 | this.checkOnSpawn(creep, onNew); 42 | } 43 | this._creep = creep.name; 44 | } 45 | 46 | get died() { 47 | return this.memory?.spawned && (!this._creep || !Game.creeps[this._creep]); 48 | } 49 | 50 | cpuRemaining(): number { 51 | return ( 52 | Math.min(CPU_ESTIMATE_PERIOD, this.resolved?.ticksToLive ?? Infinity) * this.cpuPerTick 53 | ); 54 | } 55 | 56 | ttlRemaining(): number { 57 | return Math.min(CPU_ESTIMATE_PERIOD, this.resolved?.ticksToLive ?? Infinity); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Missions/BaseClasses/CreepSpawner/MultiCreepSpawner.ts: -------------------------------------------------------------------------------- 1 | import { isCreep } from 'Selectors/typeguards'; 2 | import { CPU_ESTIMATE_PERIOD } from 'config'; 3 | import { BaseCreepSpawner } from './BaseCreepSpawner'; 4 | 5 | export class MultiCreepSpawner extends BaseCreepSpawner { 6 | constructor( 7 | id: string, 8 | office: string, 9 | public props: BaseCreepSpawner['props'] & { count: (current: Creep[]) => number }, 10 | public eventHandlers?: BaseCreepSpawner['eventHandlers'] 11 | ) { 12 | super(id, office, props, eventHandlers); 13 | } 14 | 15 | spawn(missionId: CreepMemory['missionId'], priority: number) { 16 | const spawnOrders = []; 17 | for (let i = 0; i < this.props.count(this.resolved); i += 1) { 18 | spawnOrders.push(...super.spawn(missionId, priority)); 19 | } 20 | return spawnOrders; 21 | } 22 | 23 | public _creeps: string[] = []; 24 | 25 | get resolved(): Creep[] { 26 | const creeps = this._creeps.map(n => Game.creeps[n]).filter(isCreep); 27 | this._creeps = creeps.map(c => c.name); 28 | return creeps; 29 | } 30 | 31 | register(creep: Creep, onNew?: () => void) { 32 | if (!this._creeps.includes(creep.name)) { 33 | this._creeps.push(creep.name); 34 | } 35 | this.checkOnSpawn(creep, onNew); 36 | } 37 | 38 | cpuRemaining(): number { 39 | return ( 40 | this.resolved.reduce((sum, c) => sum + Math.min(CPU_ESTIMATE_PERIOD, c.ticksToLive ?? Infinity), 0) * this.cpuPerTick 41 | ); 42 | } 43 | 44 | ttlRemaining(): number { 45 | return this.resolved.reduce((sum, c) => sum + Math.min(CPU_ESTIMATE_PERIOD, c.ticksToLive ?? Infinity), 0); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Missions/BaseClasses/MissionSpawner/BaseMissionSpawner.ts: -------------------------------------------------------------------------------- 1 | import { MissionImplementation } from '../MissionImplementation'; 2 | 3 | export class BaseMissionSpawner { 4 | register(ids: InstanceType['id'][]) {} 5 | get resolved(): InstanceType | InstanceType[] | undefined { 6 | return; 7 | } 8 | 9 | /** 10 | * Generate missions 11 | */ 12 | spawn() {} 13 | } 14 | -------------------------------------------------------------------------------- /src/Missions/BaseClasses/MissionSpawner/ConditionalMissionSpawner.ts: -------------------------------------------------------------------------------- 1 | import { MissionStatus } from 'Missions/Mission'; 2 | import { MissionImplementation } from '../MissionImplementation'; 3 | import { BaseMissionSpawner } from './BaseMissionSpawner'; 4 | 5 | export class ConditionalMissionSpawner extends BaseMissionSpawner { 6 | constructor( 7 | public missionClass: T, 8 | public missionData: () => InstanceType['missionData'], 9 | public spawnWhen: () => boolean, 10 | public onSpawn?: (mission: InstanceType) => void 11 | ) { 12 | super(); 13 | } 14 | 15 | public ids: InstanceType['id'][] = []; 16 | 17 | register(ids: InstanceType['id'][]) { 18 | this.ids = ids; 19 | this.resolved?.init(); 20 | } 21 | 22 | spawn() { 23 | let mission = this.missionClass.fromId(this.ids[0]) as InstanceType; 24 | if (!mission || mission.status === MissionStatus.CLEANUP) { 25 | this.ids.shift(); 26 | if (this.spawnWhen()) { 27 | mission = new this.missionClass(this.missionData()) as InstanceType; 28 | this.onSpawn?.(mission); 29 | this.ids.push(mission.id); 30 | } 31 | } 32 | } 33 | 34 | get resolved() { 35 | return this.missionClass.fromId(this.ids[0]) as InstanceType; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Missions/BaseClasses/MissionSpawner/MissionSpawner.ts: -------------------------------------------------------------------------------- 1 | import { MissionImplementation } from '../MissionImplementation'; 2 | import { ConditionalMissionSpawner } from './ConditionalMissionSpawner'; 3 | 4 | export class MissionSpawner extends ConditionalMissionSpawner { 5 | constructor( 6 | public missionClass: T, 7 | public missionData: () => InstanceType['missionData'], 8 | public onSpawn?: (mission: InstanceType) => void 9 | ) { 10 | super(missionClass, missionData, () => true, onSpawn); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Missions/BaseClasses/MissionSpawner/MultiMissionSpawner.ts: -------------------------------------------------------------------------------- 1 | import { MissionStatus } from 'Missions/Mission'; 2 | import { MissionImplementation } from '../MissionImplementation'; 3 | import { BaseMissionSpawner } from './BaseMissionSpawner'; 4 | 5 | export class MultiMissionSpawner extends BaseMissionSpawner { 6 | constructor( 7 | public missionClass: T, 8 | public generate: (current: InstanceType[]) => InstanceType['missionData'][], 9 | public onSpawn?: (mission: InstanceType) => void 10 | ) { 11 | super(); 12 | } 13 | 14 | public ids: InstanceType['id'][] = []; 15 | 16 | register(ids: InstanceType['id'][]) { 17 | this.ids = ids; 18 | this.resolved.forEach(m => m.init()); 19 | } 20 | 21 | spawn() { 22 | for (const data of this.generate(this.resolved)) { 23 | const mission = new this.missionClass(data) as InstanceType; 24 | this.onSpawn?.(mission); 25 | this.ids.push(mission.id); 26 | } 27 | } 28 | 29 | get resolved() { 30 | // clean up ids 31 | this.ids.forEach((id, i) => { 32 | if (!Memory.missions[id]) this.ids.splice(this.ids.indexOf(id)); 33 | }); 34 | return this.ids 35 | .map(id => this.missionClass.fromId(id) as InstanceType) 36 | .filter(mission => mission?.status !== MissionStatus.CLEANUP); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Missions/BaseClasses/index.ts: -------------------------------------------------------------------------------- 1 | export const fixedCount = (target: () => number) => (creeps: Creep[]) => target() - creeps.length; 2 | -------------------------------------------------------------------------------- /src/Missions/BaseClasses/runMissions.ts: -------------------------------------------------------------------------------- 1 | import { initializeOfficeMissions } from 'Missions/initializeOfficeMissions'; 2 | import { MISSION_HISTORY_LIMIT } from 'config'; 3 | import { debugCPU, resetDebugCPU } from 'utils/debugCPU'; 4 | import { allMissions } from './MissionImplementation'; 5 | 6 | export function runMissions() { 7 | initializeOfficeMissions(); 8 | resetDebugCPU(true); 9 | for (const mission of allMissions()) { 10 | try { 11 | mission.execute(); 12 | } catch (e) { 13 | console.log(`Error in mission ${mission.constructor.name} in room ${mission.missionData.office}: ${e}`); 14 | console.log((e as Error).stack); 15 | } 16 | debugCPU(mission.constructor.name); 17 | } 18 | // debugCPU('runMissions'); 19 | Memory.missionReports ??= []; 20 | Memory.missionReports = Memory.missionReports.filter(r => r.finished > Game.time - MISSION_HISTORY_LIMIT); 21 | } 22 | 23 | export function allocatedResources() { 24 | const orders: Record< 25 | string, 26 | { 27 | energyAllocated: number; 28 | cpuAllocated: number; 29 | } 30 | > = {}; 31 | for (const mission of allMissions()) { 32 | orders[mission.missionData.office] ??= { 33 | energyAllocated: 0, 34 | cpuAllocated: 0 35 | }; 36 | orders[mission.missionData.office].cpuAllocated += mission.cpuRemaining(); 37 | orders[mission.missionData.office].energyAllocated += mission.energyRemaining(); 38 | } 39 | return orders; 40 | } 41 | -------------------------------------------------------------------------------- /src/Missions/Budgets.ts: -------------------------------------------------------------------------------- 1 | import { rcl } from 'Selectors/rcl'; 2 | import { roomPlans } from 'Selectors/roomPlans'; 3 | 4 | export function getWithdrawLimit(office: string, budget: Budget) { 5 | return getBudgetAdjustment(office, budget); 6 | } 7 | 8 | export enum Budget { 9 | ESSENTIAL = 'ESSENTIAL', 10 | ECONOMY = 'ECONOMY', 11 | EFFICIENCY = 'EFFICIENCY', 12 | SURPLUS = 'SURPLUS' 13 | } 14 | 15 | /** 16 | * Sets capacity threshold for different mission types, to make sure certain 17 | * missions can spawn only when storage levels are high enough - storage must 18 | * have `capacity` + `missionCost` to spawn mission 19 | */ 20 | export function getBudgetAdjustment(office: string, budget: Budget) { 21 | if (!roomPlans(office)?.headquarters?.storage.structure) { 22 | // No storage yet - minimal capacities enforced, except for income missions 23 | if ([Budget.ESSENTIAL, Budget.ECONOMY].includes(budget)) { 24 | return -Infinity; 25 | } else if ([Budget.SURPLUS].includes(budget)) { 26 | return Game.rooms[office].energyCapacityAvailable; 27 | } else { 28 | return 0; 29 | } 30 | } else if (rcl(office) < 8) { 31 | // Storage allows more fine-grained capacity management 32 | if ([Budget.ESSENTIAL, Budget.ECONOMY].includes(budget)) { 33 | return -Infinity; 34 | } else if ([Budget.EFFICIENCY].includes(budget)) { 35 | return Game.rooms[office].energyCapacityAvailable * (rcl(office) / 2); 36 | } else if ([Budget.SURPLUS].includes(budget)) { 37 | return 100000; 38 | } else { 39 | return 60000; 40 | } 41 | } else { 42 | // At RCL8 we bump up the storage reservoir 43 | if ([Budget.ESSENTIAL, Budget.ECONOMY].includes(budget)) { 44 | return -Infinity; 45 | } else if ([Budget.EFFICIENCY].includes(budget)) { 46 | return 60000; 47 | } else if ([Budget.SURPLUS].includes(budget)) { 48 | return 200000; 49 | } else { 50 | return 100000; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Missions/Implementations/DefendAcquireMission.ts: -------------------------------------------------------------------------------- 1 | import { BaseDuoMission, BaseDuoMissionData } from 'Missions/BaseClasses/BaseDuoMission'; 2 | import { priorityKillTarget } from 'Selectors/Combat/priorityTarget'; 3 | import { packPos } from 'utils/packrat'; 4 | import { ResolvedCreeps, ResolvedMissions } from '../BaseClasses/MissionImplementation'; 5 | 6 | export interface DefendAcquireMissionData extends BaseDuoMissionData { 7 | targetOffice: string; 8 | arrived?: number; 9 | } 10 | 11 | export class DefendAcquireMission extends BaseDuoMission { 12 | priority = 7.7; 13 | 14 | constructor(public missionData: DefendAcquireMissionData, id?: string) { 15 | super(missionData, id); 16 | } 17 | static fromId(id: DefendAcquireMission['id']) { 18 | return super.fromId(id) as DefendAcquireMission; 19 | } 20 | 21 | run( 22 | creeps: ResolvedCreeps, 23 | missions: ResolvedMissions, 24 | data: DefendAcquireMissionData 25 | ) { 26 | data.killTarget = priorityKillTarget(data.targetOffice)?.id; 27 | data.rallyPoint ??= { 28 | pos: packPos(new RoomPosition(25, 25, data.targetOffice)), 29 | range: 20 30 | }; 31 | 32 | if (creeps.attacker?.pos.roomName === data.targetOffice) { 33 | data.arrived ??= CREEP_LIFE_TIME - (creeps.attacker.ticksToLive ?? 100); 34 | } 35 | 36 | super.run(creeps, missions, data); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Missions/Implementations/DefendOfficeMission.ts: -------------------------------------------------------------------------------- 1 | import { BaseDuoMission, BaseDuoMissionData } from 'Missions/BaseClasses/BaseDuoMission'; 2 | import { rampartsAreBroken } from 'Selectors/Combat/defenseRamparts'; 3 | import { priorityKillTarget } from 'Selectors/Combat/priorityTarget'; 4 | import { roomPlans } from 'Selectors/roomPlans'; 5 | import { packPos } from 'utils/packrat'; 6 | import { ResolvedCreeps, ResolvedMissions } from '../BaseClasses/MissionImplementation'; 7 | 8 | export interface DefendOfficeMissionData extends BaseDuoMissionData {} 9 | 10 | export class DefendOfficeMission extends BaseDuoMission { 11 | priority = 15; 12 | 13 | constructor(public missionData: DefendOfficeMissionData, id?: string) { 14 | super(missionData, id); 15 | } 16 | static fromId(id: DefendOfficeMission['id']) { 17 | return super.fromId(id) as DefendOfficeMission; 18 | } 19 | 20 | run( 21 | creeps: ResolvedCreeps, 22 | missions: ResolvedMissions, 23 | data: DefendOfficeMissionData 24 | ) { 25 | const storagePos = roomPlans(data.office)?.headquarters?.storage.pos ?? new RoomPosition(25, 25, data.office); 26 | data.rallyPoint = rampartsAreBroken(data.office) ? { pos: packPos(storagePos), range: 10 } : undefined; 27 | data.stayInRamparts = true; 28 | data.killTarget = priorityKillTarget(data.office)?.id; 29 | 30 | super.run(creeps, missions, data); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Missions/Implementations/EmergencyUpgradeMission.ts: -------------------------------------------------------------------------------- 1 | import { buildAccountant } from 'Minions/Builds/accountant'; 2 | import { buildResearch } from 'Minions/Builds/research'; 3 | import { MinionTypes } from 'Minions/minionTypes'; 4 | import { MultiCreepSpawner } from 'Missions/BaseClasses/CreepSpawner/MultiCreepSpawner'; 5 | import { ResolvedCreeps, ResolvedMissions } from 'Missions/BaseClasses/MissionImplementation'; 6 | import { Budget } from 'Missions/Budgets'; 7 | import { MissionStatus } from 'Missions/Mission'; 8 | import { rcl } from 'Selectors/rcl'; 9 | import { UpgradeMission, UpgradeMissionData } from './UpgradeMission'; 10 | 11 | export class EmergencyUpgradeMission extends UpgradeMission { 12 | budget = Budget.ESSENTIAL; 13 | public creeps = { 14 | upgraders: new MultiCreepSpawner('u', this.missionData.office, { 15 | role: MinionTypes.RESEARCH, 16 | budget: this.budget, 17 | builds: energy => buildResearch(energy), 18 | count: current => { 19 | if (!EmergencyUpgradeMission.shouldRun(this.missionData.office) || current.length) return 0; 20 | return 1; 21 | } 22 | }), 23 | haulers: new MultiCreepSpawner('h', this.missionData.office, { 24 | role: MinionTypes.ACCOUNTANT, 25 | budget: this.budget, 26 | builds: energy => buildAccountant(energy, 25, true, false), 27 | count: () => 0, // don't spawn haulers for emergency upgrade 28 | estimatedCpuPerTick: 1 29 | }) 30 | }; 31 | 32 | static shouldRun(office: string) { 33 | return ( 34 | Game.rooms[office].controller!.ticksToDowngrade < 3000 || 35 | rcl(office) < Math.max(...Object.keys(Memory.rooms[office].rclMilestones ?? {}).map(Number)) 36 | ); 37 | } 38 | 39 | priority = 15; 40 | static fromId(id: EmergencyUpgradeMission['id']) { 41 | return super.fromId(id) as EmergencyUpgradeMission; 42 | } 43 | 44 | run(creeps: ResolvedCreeps, missions: ResolvedMissions, data: UpgradeMissionData) { 45 | super.run(creeps, missions, data); 46 | if (!EmergencyUpgradeMission.shouldRun(data.office) && !creeps.upgraders.length) { 47 | this.status = MissionStatus.DONE; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Missions/MemoryFixes.ts: -------------------------------------------------------------------------------- 1 | Memory.missions ??= {}; 2 | Memory.missionReports ??= []; 3 | -------------------------------------------------------------------------------- /src/Missions/Mission.ts: -------------------------------------------------------------------------------- 1 | export enum MissionStatus { 2 | PENDING = 'PENDING', 3 | RUNNING = 'RUNNING', 4 | DONE = 'DONE', 5 | CLEANUP = 'CLEANUP' 6 | } 7 | 8 | export function generateMissionId() { 9 | return Number(Math.floor(Math.random() * 0xffffffff)) 10 | .toString(16) 11 | .padStart(8, '0'); 12 | } 13 | -------------------------------------------------------------------------------- /src/Missions/Selectors/index.ts: -------------------------------------------------------------------------------- 1 | import { allMissions, missionById, MissionImplementation } from 'Missions/BaseClasses/MissionImplementation'; 2 | import { MissionStatus } from 'Missions/Mission'; 3 | import { averageActiveFranchiseRoundTripDistance } from 'Selectors/Franchises/furthestActiveFranchiseRoundTripDistance'; 4 | import { roomPlans } from 'Selectors/roomPlans'; 5 | import { memoizeByTick } from 'utils/memoizeFunction'; 6 | 7 | export function isMission(missionType: T) { 8 | return (mission: MissionImplementation): mission is InstanceType => mission instanceof missionType; 9 | } 10 | 11 | export function and(...conditions: ((t: T) => boolean)[]) { 12 | return (t: T) => conditions.every(c => c(t)); 13 | } 14 | 15 | export function or(...conditions: ((t: T) => boolean)[]) { 16 | return (t: T) => conditions.some(c => c(t)); 17 | } 18 | 19 | export function not(condition: (t: T) => boolean) { 20 | return (t: T) => !condition(t); 21 | } 22 | 23 | export function isStatus(status: MissionStatus) { 24 | return (mission: { status: MissionStatus }) => mission.status === status; 25 | } 26 | 27 | export function assignedMission(creep: Creep): MissionImplementation | undefined { 28 | return creep.memory.missionId ? missionById(creep.memory.missionId) : undefined; 29 | } 30 | 31 | export function estimateMissionInterval(office: string) { 32 | if (roomPlans(office)?.headquarters?.storage.structure) { 33 | return CREEP_LIFE_TIME; 34 | } else { 35 | return Math.max(50, averageActiveFranchiseRoundTripDistance(office) * 1.2); // This worked best in my tests to balance income with expenses 36 | } 37 | } 38 | 39 | export const missionsByOffice = memoizeByTick( 40 | () => '', 41 | () => { 42 | const missions: Record = {}; 43 | for (const mission of allMissions()) { 44 | missions[mission.missionData.office] ??= []; 45 | missions[mission.missionData.office].push(mission); 46 | } 47 | return missions; 48 | } 49 | ); 50 | 51 | export const activeMissions = (office: string) => missionsByOffice()[office] ?? []; 52 | -------------------------------------------------------------------------------- /src/Missions/initializeOfficeMissions.ts: -------------------------------------------------------------------------------- 1 | import { missionById } from './BaseClasses/MissionImplementation'; 2 | import { MainOfficeMission } from './Implementations/MainOfficeMission'; 3 | 4 | declare global { 5 | interface OfficeMemory { 6 | missionId?: MainOfficeMission['id']; 7 | } 8 | } 9 | 10 | let initializedCreeps = false; 11 | const officeMissions = new Map(); 12 | export function initializeOfficeMissions() { 13 | for (const office in Memory.offices) { 14 | if (Memory.roomPlans[office]?.complete && !officeMissions.has(office)) { 15 | const mission = new MainOfficeMission({ office }, Memory.offices[office].missionId); 16 | mission.init(); 17 | Memory.offices[office].missionId = mission.id; 18 | officeMissions.set(office, mission); 19 | } 20 | } 21 | if (!initializedCreeps) { 22 | initializedCreeps = true; 23 | // register creeps 24 | for (let creep in Game.creeps) { 25 | const mission = missionById(Game.creeps[creep].memory.missionId.split('|')[0]); 26 | mission?.register(Game.creeps[creep]); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Reports/EnergyReport.ts: -------------------------------------------------------------------------------- 1 | import { roomPlans } from "Selectors/roomPlans"; 2 | 3 | export default () => { 4 | for (let office in Memory.offices) { 5 | const plan = roomPlans(office); 6 | const storage = plan?.headquarters?.storage.structure as StructureStorage|undefined; 7 | const 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Reports/MilestonesReport.ts: -------------------------------------------------------------------------------- 1 | import { Dashboard, Rectangle, Table } from "screeps-viz"; 2 | 3 | export default () => { 4 | for (let room in Memory.offices) { 5 | const rclMilestones = Memory.rooms[room].rclMilestones; 6 | if (!rclMilestones) continue; 7 | const rclMilestonesTable = []; 8 | for (let i = 1; i <= 8; i++) { 9 | rclMilestonesTable.push([ 10 | i, 11 | (rclMilestones[i] - (rclMilestones[i - 1] ?? rclMilestones[1])) ?? '', 12 | (rclMilestones[i] - rclMilestones[1]) ?? '', 13 | ]) 14 | } 15 | 16 | const gclMilestones = Memory.stats.gclMilestones; 17 | if (!gclMilestones) continue; 18 | const gclMilestonesTable = []; 19 | for (let i = 1; i <= Object.keys(gclMilestones).length; i++) { 20 | gclMilestonesTable.push([ 21 | i, 22 | (gclMilestones[i] - (gclMilestones[i - 1] ?? gclMilestones[1])) ?? '', 23 | (gclMilestones[i] - gclMilestones[1]) ?? '', 24 | ]) 25 | } 26 | 27 | Dashboard({ 28 | widgets: [ 29 | { 30 | pos: { x: 1, y: 1 }, 31 | width: 15, 32 | height: 21, 33 | widget: Rectangle({ data: Table({ 34 | config: { headers: ['Level', 'From Previous', 'From First'], label: 'GCL' }, 35 | data: gclMilestonesTable 36 | }) }) 37 | }, 38 | { 39 | pos: { x: 17, y: 1 }, 40 | width: 15, 41 | height: 11, 42 | widget: Rectangle({ data: Table({ 43 | config: { headers: ['Level', 'From Previous', 'From First'], label: 'RCL' }, 44 | data: rclMilestonesTable 45 | }) }) 46 | }, 47 | ], 48 | config: { room } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Reports/OfficeReport.ts: -------------------------------------------------------------------------------- 1 | import { MineMission } from 'Missions/Implementations/MineMission'; 2 | import { ScienceMission } from 'Missions/Implementations/ScienceMission'; 3 | import { activeMissions, isMission } from 'Missions/Selectors'; 4 | import { rcl } from 'Selectors/rcl'; 5 | 6 | // Show current priority: building, repairing, filling storage, upgrading, acquiring 7 | // Show secondary activities: mining, labs/boosting 8 | // Summarize economy state: franchise income, total budgets 9 | // Show RCL meter 10 | // Show this on the world map 11 | 12 | export default () => { 13 | for (let office in Memory.offices) { 14 | Game.map.visual.text(rcl(office).toString(), new RoomPosition(25, 25, office)); 15 | 16 | let currentPriority = ''; 17 | let mining = false; 18 | let science = false; 19 | const meterProgress = Game.rooms[office].controller!.progress / Game.rooms[office].controller!.progressTotal; 20 | const meterMessage = `${(meterProgress * 100).toFixed(0)}%`; 21 | const meterColor = '#0000ff'; 22 | for (const mission of activeMissions(office)) { 23 | if (isMission(MineMission)(mission)) mining = true; 24 | if (isMission(ScienceMission)(mission)) science = true; 25 | } 26 | if (currentPriority === '') continue; 27 | 28 | if (mining) { 29 | Game.map.visual.text('⛏', new RoomPosition(40, 10, office)); 30 | } 31 | if (science) { 32 | Game.map.visual.text('🔬', new RoomPosition(30, 10, office)); 33 | } 34 | 35 | // Draw meter 36 | Game.map.visual.rect(new RoomPosition(0, 40, office), 50, 10, { stroke: meterColor, fill: 'transparent' }); 37 | Game.map.visual.rect(new RoomPosition(0, 40, office), 50 * Math.max(0, Math.min(1, meterProgress)), 10, { 38 | stroke: 'transparent', 39 | fill: meterColor 40 | }); 41 | Game.map.visual.text(meterMessage, new RoomPosition(25, 45, office)); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/Reports/SpawnReport.ts: -------------------------------------------------------------------------------- 1 | import { missionById } from 'Missions/BaseClasses/MissionImplementation'; 2 | import { getBudgetAdjustment } from 'Missions/Budgets'; 3 | import { spawnRequests } from 'Missions/Control'; 4 | import { activeMissions } from 'Missions/Selectors'; 5 | import { Dashboard, Rectangle, Table } from 'screeps-viz'; 6 | import { MissionEnergyAvailable } from 'Selectors/Missions/missionEnergyAvailable'; 7 | import { sum } from 'Selectors/reducers'; 8 | 9 | export default () => { 10 | for (const room in Memory.offices ?? []) { 11 | let table = []; 12 | const data = (spawnRequests.get(room) ?? []).sort((a, b) => b.priority - a.priority); 13 | const energy = 14 | MissionEnergyAvailable[room] - 15 | activeMissions(room) 16 | .map(m => m.energyRemaining()) 17 | .reduce(sum, 0); 18 | for (let s of data) { 19 | const mission = missionById(s.memory.missionId.split('|')[0])?.constructor.name; 20 | table.push([ 21 | mission, 22 | s.name, 23 | s.memory.role, 24 | s.priority, 25 | s.budget, 26 | s.builds.length, 27 | s.builds 28 | .map(build => (energy - s.estimate(build).energy - getBudgetAdjustment(s.office, s.budget)).toFixed(0)) 29 | .join('/') 30 | ]); 31 | } 32 | console.log(table.map(r => JSON.stringify(r)).join('\n')) 33 | Dashboard({ 34 | widgets: [ 35 | { 36 | pos: { x: 1, y: 1 }, 37 | width: 48, 38 | height: 2 + Math.min(48, table.length * 1.5), 39 | widget: Rectangle({ 40 | data: Table({ 41 | config: { headers: ['Mission', 'Minion', 'Role', 'Priority', 'Budget', 'Builds', 'Energy'] }, 42 | data: table 43 | }) 44 | }) 45 | } 46 | ], 47 | config: { room } 48 | }); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /src/Reports/TerminalsReport.ts: -------------------------------------------------------------------------------- 1 | import { officeResourceSurplus } from "Selectors/officeResourceSurplus"; 2 | 3 | export default () => { 4 | for (let office in Memory.offices) { 5 | const surpluses = officeResourceSurplus(office); 6 | let rows = 0; 7 | for (let [resource, amount] of surpluses) { 8 | const length = (25 * (amount / 5000)) 9 | let color = (amount >= 0) ? '#00ff00' : '#ff0000'; 10 | 11 | Game.map.visual.rect( 12 | new RoomPosition(25, rows * 5, office), 13 | Math.min(25, length), 14 | 5, 15 | { stroke: color, fill: color } 16 | ) 17 | Game.map.visual.text(resource, new RoomPosition(25, rows * 5 + 2, office), { fontSize: 4 }); 18 | rows += 1; 19 | if (rows >= 10) break; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Reports/fastfillerPositions.ts: -------------------------------------------------------------------------------- 1 | import { rcl } from 'Selectors/rcl'; 2 | import { roomPlans } from 'Selectors/roomPlans'; 3 | import { packPos } from 'utils/packrat'; 4 | 5 | export function fastfillerPositions(room: string, effectiveRcl = rcl(room)) { 6 | const spawns = roomPlans(room)?.fastfiller?.spawns; 7 | if (!spawns) return []; 8 | const [spawn1, spawn2, spawn3] = spawns.map(s => s.pos); 9 | return [ 10 | new RoomPosition(spawn1.x + 1, spawn1.y, spawn1.roomName), 11 | new RoomPosition(spawn3.x - 1, spawn3.y - 1, spawn3.roomName), 12 | new RoomPosition(spawn2.x - 1, spawn2.y, spawn2.roomName), 13 | new RoomPosition(spawn3.x + 1, spawn3.y - 1, spawn3.roomName) 14 | ].slice(0, effectiveRcl < 3 ? 2 : undefined); // only fill the first two fastiller positions until RCL3 15 | } 16 | 17 | export function refillSquares(room: string) { 18 | const [topLeft, bottomLeft, topRight, bottomRight] = fastfillerPositions(room, 8).map(p => packPos(p)); 19 | return { 20 | topLeft, 21 | bottomLeft, 22 | topRight, 23 | bottomRight 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/RoomPlanner/Algorithms/flowfield.ts: -------------------------------------------------------------------------------- 1 | import { calculateNearbyPositions, terrainCostAt } from 'Selectors/Map/MapCoordinates'; 2 | import { memoize } from 'utils/memoizeFunction'; 3 | import { Coord } from 'utils/packrat'; 4 | 5 | export function flowfields(room: string, pointsOfInterest: Record, obstacles?: CostMatrix) { 6 | const flowfields: Record = {}; 7 | for (const poi in pointsOfInterest) { 8 | flowfields[poi] = dijkstraMap( 9 | room, 10 | pointsOfInterest[poi].map(p => new RoomPosition(p.x, p.y, room)), 11 | obstacles 12 | ); 13 | } 14 | return flowfields; 15 | } 16 | 17 | export const dijkstraMap = memoize( 18 | (room: string, source: RoomPosition[], obstacles?: CostMatrix, useTerrainCost = true) => 19 | `${room}_${source}_${useTerrainCost}`, 20 | (room: string, source: RoomPosition[], obstacles = new PathFinder.CostMatrix(), useTerrainCost = true) => { 21 | const frontier = source.slice(); 22 | const terrain = Game.map.getRoomTerrain(room); 23 | const cm = new PathFinder.CostMatrix(); 24 | 25 | while (frontier.length) { 26 | const current = frontier.shift()!; 27 | for (const next of calculateNearbyPositions(current, 1)) { 28 | if ( 29 | terrain.get(next.x, next.y) === TERRAIN_MASK_WALL || 30 | obstacles.get(next.x, next.y) === 255 || 31 | source.some(s => s.isEqualTo(next)) 32 | ) 33 | continue; 34 | 35 | const nextCost = cm.get(current.x, current.y) + (useTerrainCost ? terrainCostAt(next) : 1); 36 | 37 | if (cm.get(next.x, next.y) === 0 || cm.get(next.x, next.y) > nextCost) { 38 | frontier.push(next); 39 | cm.set(next.x, next.y, nextCost); 40 | } 41 | } 42 | } 43 | 44 | return cm; 45 | } 46 | ); 47 | -------------------------------------------------------------------------------- /src/RoomPlanner/Algorithms/mincut.d.ts: -------------------------------------------------------------------------------- 1 | export interface Rect {x1: number, x2: number, y1: number, y2: number} 2 | export interface Tile {x: number, y: number} 3 | 4 | export function create_graph(roomname: string, rect: Rect, bounds: Rect): void 5 | export function delete_tiles_to_dead_ends(roomname: string, cut_tiles_array: Tile[]): void 6 | export function GetCutTiles(roomname: string, rect: Rect[], bounds?: Rect, verbose?: boolean): Tile[] 7 | export function test(roomname: string): string 8 | -------------------------------------------------------------------------------- /src/RoomPlanner/Backfill/deserializeBackfillPlan.ts: -------------------------------------------------------------------------------- 1 | import type { BackfillPlan } from 'RoomPlanner'; 2 | import { deserializePlannedStructures } from 'Selectors/plannedStructures'; 3 | import { isPlannedStructure } from 'Selectors/typeguards'; 4 | import { validateBackfillPlan } from './validateBackfillPlan'; 5 | 6 | export function deserializeBackfillPlan(serialized: string) { 7 | const plan: Partial = { 8 | extensions: [], 9 | towers: [], 10 | ramparts: [], 11 | observer: undefined 12 | }; 13 | for (const s of deserializePlannedStructures(serialized)) { 14 | if (isPlannedStructure(STRUCTURE_EXTENSION)(s)) plan.extensions?.push(s); 15 | if (isPlannedStructure(STRUCTURE_TOWER)(s)) plan.towers?.push(s); 16 | if (isPlannedStructure(STRUCTURE_RAMPART)(s)) plan.ramparts?.push(s); 17 | if (isPlannedStructure(STRUCTURE_OBSERVER)(s)) plan.observer = s; 18 | } 19 | return validateBackfillPlan(plan); 20 | } 21 | -------------------------------------------------------------------------------- /src/RoomPlanner/Backfill/validateBackfillPlan.ts: -------------------------------------------------------------------------------- 1 | import { BackfillPlan } from 'RoomPlanner'; 2 | 3 | export function validateBackfillPlan(plan: Partial) { 4 | if (!plan.towers?.length || !plan.observer) { 5 | throw new Error(`Incomplete BackfillPlan`); 6 | } else { 7 | return plan as BackfillPlan; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/RoomPlanner/Extensions/deserializeExtensionsPlan.ts: -------------------------------------------------------------------------------- 1 | import type { ExtensionsPlan } from 'RoomPlanner'; 2 | import { deserializePlannedStructures } from 'Selectors/plannedStructures'; 3 | import { isPlannedStructure } from 'Selectors/typeguards'; 4 | import { validateExtensionsPlan } from './validateExtensionsPlan'; 5 | 6 | export function deserializeExtensionsPlan(serialized: string) { 7 | const plan: Partial = { 8 | extensions: [], 9 | roads: [], 10 | ramparts: [] 11 | }; 12 | for (const s of deserializePlannedStructures(serialized)) { 13 | if (isPlannedStructure(STRUCTURE_EXTENSION)(s)) plan.extensions?.push(s); 14 | if (isPlannedStructure(STRUCTURE_ROAD)(s)) plan.roads?.push(s); 15 | if (isPlannedStructure(STRUCTURE_RAMPART)(s)) plan.ramparts?.push(s); 16 | } 17 | return validateExtensionsPlan(plan); 18 | } 19 | -------------------------------------------------------------------------------- /src/RoomPlanner/Extensions/validateExtensionsPlan.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionsPlan } from 'RoomPlanner'; 2 | 3 | export function validateExtensionsPlan(plan: Partial) { 4 | if (!plan.extensions?.length || !plan.roads?.length) { 5 | throw new Error(`Incomplete ExtensionsPlan`); 6 | } else { 7 | return plan as ExtensionsPlan; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/RoomPlanner/Franchise/deserializeFranchisePlan.ts: -------------------------------------------------------------------------------- 1 | import type { FranchisePlan } from 'RoomPlanner'; 2 | import { deserializePlannedStructures } from 'Selectors/plannedStructures'; 3 | import { isPlannedStructure } from 'Selectors/typeguards'; 4 | import { validateFranchisePlan } from './validateFranchisePlan'; 5 | 6 | export function deserializeFranchisePlan(serialized: string) { 7 | const plan: Partial = { 8 | sourceId: serialized.slice(0, 24).trim() as Id, 9 | link: undefined, 10 | container: undefined, 11 | extensions: [], 12 | ramparts: [] 13 | }; 14 | for (const s of deserializePlannedStructures(serialized.slice(24))) { 15 | if (isPlannedStructure(STRUCTURE_LINK)(s)) plan.link = s; 16 | if (isPlannedStructure(STRUCTURE_CONTAINER)(s)) plan.container = s; 17 | if (isPlannedStructure(STRUCTURE_RAMPART)(s)) plan.ramparts?.push(s); 18 | if (isPlannedStructure(STRUCTURE_EXTENSION)(s)) plan.extensions?.push(s); 19 | } 20 | return validateFranchisePlan(plan); 21 | } 22 | -------------------------------------------------------------------------------- /src/RoomPlanner/Franchise/serializeFranchisePlan.ts: -------------------------------------------------------------------------------- 1 | import { FranchisePlan } from "RoomPlanner"; 2 | import { serializePlannedStructures } from "Selectors/plannedStructures"; 3 | import { EMPTY_ID } from "./FranchisePlan"; 4 | 5 | 6 | export function serializeFranchisePlan(plan?: FranchisePlan) { 7 | if (!plan) throw new Error('Undefined FranchisePlan, cannot serialize'); 8 | const { sourceId, ...structures } = plan; 9 | return sourceId + EMPTY_ID.slice(sourceId.length) + serializePlannedStructures(Object.values(structures).flat()); 10 | } 11 | -------------------------------------------------------------------------------- /src/RoomPlanner/Franchise/validateFranchisePlan.ts: -------------------------------------------------------------------------------- 1 | import { FranchisePlan } from 'RoomPlanner'; 2 | 3 | export function validateFranchisePlan(plan: Partial) { 4 | if ( 5 | !plan.sourceId || 6 | !plan.link || 7 | !plan.container // || !plan.ramparts?.length 8 | ) { 9 | throw new Error(`Incomplete FranchisePlan`); 10 | } else { 11 | return plan as FranchisePlan; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/RoomPlanner/Library/LibraryPlan.ts: -------------------------------------------------------------------------------- 1 | import { LibraryPlan } from 'RoomPlanner'; 2 | import { PlannedStructure } from 'RoomPlanner/PlannedStructure'; 3 | import { 4 | adjacentWalkablePositions, 5 | calculateNearbyPositions, 6 | getRangeTo, 7 | isPositionWalkable 8 | } from 'Selectors/Map/MapCoordinates'; 9 | import { controllerPosition } from 'Selectors/roomCache'; 10 | import { viz } from 'Selectors/viz'; 11 | import { validateLibraryPlan } from './validateLibraryPlan'; 12 | 13 | // high score wins 14 | const scorePos = (pos: RoomPosition) => 15 | calculateNearbyPositions(pos, 2, false).filter(pos => isPositionWalkable(pos, true, true)).length; 16 | 17 | export const planLibrary = (roomName: string) => { 18 | const plan: Partial = { 19 | container: undefined, 20 | link: undefined 21 | }; 22 | 23 | const controller = controllerPosition(roomName); 24 | 25 | if (!controller) throw new Error('Unable to plan Library without controller'); 26 | 27 | plan.walls = adjacentWalkablePositions(controller, true).map(pos => new PlannedStructure(pos, STRUCTURE_WALL)); 28 | 29 | const viableSquares = calculateNearbyPositions(controller, 2, false).filter(p => isPositionWalkable(p, true, true)); 30 | const containerPos = viableSquares.reduce((a, b) => (scorePos(a) >= scorePos(b) ? a : b)); 31 | const linkPos = adjacentWalkablePositions(containerPos, true) 32 | .filter(p => getRangeTo(p, controller) === 2) 33 | .reduce((a, b) => (scorePos(a) >= scorePos(b) ? a : b)); 34 | 35 | if (containerPos) { 36 | plan.container = new PlannedStructure(containerPos, STRUCTURE_CONTAINER); 37 | viz(roomName).circle(containerPos, { fill: 'transparent', stroke: 'yellow', radius: 0.5 }); 38 | } 39 | if (linkPos) { 40 | plan.link = new PlannedStructure(linkPos, STRUCTURE_LINK); 41 | viz(roomName).circle(linkPos, { fill: 'transparent', stroke: 'green', radius: 0.5 }); 42 | } 43 | 44 | return validateLibraryPlan(plan); 45 | }; 46 | -------------------------------------------------------------------------------- /src/RoomPlanner/Library/deserializeLibraryPlan.ts: -------------------------------------------------------------------------------- 1 | import type { LibraryPlan } from 'RoomPlanner'; 2 | import { deserializePlannedStructures } from 'Selectors/plannedStructures'; 3 | import { isPlannedStructure } from 'Selectors/typeguards'; 4 | import { validateLibraryPlan } from './validateLibraryPlan'; 5 | 6 | export function deserializeLibraryPlan(serialized: string) { 7 | const plan: Partial = { 8 | container: undefined, 9 | link: undefined 10 | }; 11 | for (const s of deserializePlannedStructures(serialized)) { 12 | if (isPlannedStructure(STRUCTURE_CONTAINER)(s)) plan.container = s; 13 | if (isPlannedStructure(STRUCTURE_LINK)(s)) plan.link = s; 14 | } 15 | return validateLibraryPlan(plan); 16 | } 17 | -------------------------------------------------------------------------------- /src/RoomPlanner/Library/validateLibraryPlan.ts: -------------------------------------------------------------------------------- 1 | import { LibraryPlan } from 'RoomPlanner'; 2 | 3 | export function validateLibraryPlan(plan: Partial) { 4 | if (!plan.container || !plan.link) { 5 | throw new Error(`Incomplete LibraryPlan`); 6 | } else { 7 | return plan as LibraryPlan; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/RoomPlanner/Mine/MinePlan.ts: -------------------------------------------------------------------------------- 1 | import { MinePlan } from "RoomPlanner"; 2 | import { PlannedStructure } from "RoomPlanner/PlannedStructure"; 3 | import { costMatrixFromRoomPlan } from "Selectors/costMatrixFromRoomPlan"; 4 | import { controllerPosition, mineralPosition } from "Selectors/roomCache"; 5 | import { validateMinePlan } from "./validateMinePlan"; 6 | 7 | export const planMine = (room: string) => { 8 | const mineralPos = mineralPosition(room); 9 | if (!mineralPos) throw new Error('No known mineral in room, unable to compute plan') 10 | const plan: Partial = { 11 | extractor: undefined, 12 | container: undefined, 13 | } 14 | // Calculate from scratch 15 | let controllerPos = controllerPosition(room); 16 | if (!controllerPos) throw new Error('No known controller in room, unable to compute plan') 17 | 18 | // 1. The Mine containers will be at the first position of the path between the Mineral and the Controller. 19 | let route = PathFinder.search( 20 | mineralPos, 21 | {pos: controllerPos, range: 1}, 22 | { 23 | maxRooms: 1, 24 | roomCallback: (roomName) => { 25 | if (roomName !== room) return false; 26 | return costMatrixFromRoomPlan(roomName) 27 | } 28 | } 29 | ); 30 | if (route.incomplete) throw new Error('Unable to calculate path between source and controller'); 31 | 32 | plan.container = new PlannedStructure(route.path[0], STRUCTURE_CONTAINER); 33 | // 2. The Mineral extractor will be on the mineral itself 34 | plan.extractor = new PlannedStructure(mineralPos, STRUCTURE_EXTRACTOR); 35 | 36 | return validateMinePlan(plan); 37 | } 38 | -------------------------------------------------------------------------------- /src/RoomPlanner/Mine/deserializeMinePlan.ts: -------------------------------------------------------------------------------- 1 | import type { MinePlan } from 'RoomPlanner'; 2 | import { deserializePlannedStructures } from 'Selectors/plannedStructures'; 3 | import { isPlannedStructure } from 'Selectors/typeguards'; 4 | import { validateMinePlan } from './validateMinePlan'; 5 | 6 | export function deserializeMinePlan(serialized: string) { 7 | const plan: Partial = { 8 | extractor: undefined, 9 | container: undefined 10 | }; 11 | for (const s of deserializePlannedStructures(serialized)) { 12 | if (isPlannedStructure(STRUCTURE_EXTRACTOR)(s)) plan.extractor = s; 13 | if (isPlannedStructure(STRUCTURE_CONTAINER)(s)) plan.container = s; 14 | } 15 | return validateMinePlan(plan); 16 | } 17 | -------------------------------------------------------------------------------- /src/RoomPlanner/Mine/validateMinePlan.ts: -------------------------------------------------------------------------------- 1 | import { MinePlan } from "RoomPlanner"; 2 | 3 | 4 | export function validateMinePlan(plan: Partial) { 5 | if (!plan.extractor || !plan.container) { 6 | throw new Error(`Incomplete MinePlan`); 7 | } else { 8 | return plan as MinePlan; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/RoomPlanner/Perimeter/deserializePerimeterPlan.ts: -------------------------------------------------------------------------------- 1 | import type { PerimeterPlan } from 'RoomPlanner'; 2 | import { deserializePlannedStructures } from 'Selectors/plannedStructures'; 3 | import { validatePerimeterPlan } from "./validatePerimeterPlan"; 4 | 5 | 6 | export function deserializePerimeterPlan(serialized: string) { 7 | const plan: PerimeterPlan = { 8 | ramparts: [] 9 | }; 10 | for (const s of deserializePlannedStructures(serialized)) { 11 | if (s.structureType === STRUCTURE_RAMPART) 12 | plan.ramparts.push(s); 13 | } 14 | return validatePerimeterPlan(plan); 15 | } 16 | -------------------------------------------------------------------------------- /src/RoomPlanner/Perimeter/validatePerimeterPlan.ts: -------------------------------------------------------------------------------- 1 | import { PerimeterPlan } from 'RoomPlanner'; 2 | 3 | 4 | export function validatePerimeterPlan(plan: Partial) { 5 | if (!plan.ramparts?.length) { 6 | throw new Error(`Incomplete PerimeterPlan`); 7 | } else { 8 | return plan as PerimeterPlan; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/RoomPlanner/Roads/deserializeRoadsPlan.ts: -------------------------------------------------------------------------------- 1 | import type { RoadsPlan } from "RoomPlanner"; 2 | import { deserializePlannedStructures } from "Selectors/plannedStructures"; 3 | import { isPlannedStructure } from "Selectors/typeguards"; 4 | import { validateRoadsPlan } from "./validateRoadsPlan"; 5 | 6 | 7 | export function deserializeRoadsPlan(serialized: string) { 8 | const plan: Partial = { 9 | roads: [], 10 | }; 11 | for (const s of deserializePlannedStructures(serialized)) { 12 | if (isPlannedStructure(STRUCTURE_ROAD)(s)) 13 | plan.roads?.push(s); 14 | } 15 | return validateRoadsPlan(plan); 16 | } 17 | -------------------------------------------------------------------------------- /src/RoomPlanner/Roads/validateRoadsPlan.ts: -------------------------------------------------------------------------------- 1 | import { RoadsPlan } from "RoomPlanner"; 2 | 3 | 4 | export function validateRoadsPlan(plan: Partial) { 5 | if (!plan.roads?.length) { 6 | throw new Error(`Incomplete RoadsPlan`); 7 | } else { 8 | return plan as RoadsPlan; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/RoomPlanner/Stamps/deserializeFastfillerPlan.ts: -------------------------------------------------------------------------------- 1 | import type { FastfillerPlan } from 'RoomPlanner'; 2 | import { deserializePlannedStructures } from 'Selectors/plannedStructures'; 3 | import { isPlannedStructure } from 'Selectors/typeguards'; 4 | import { validateFastfillerPlan } from './validateFastfillerPlan'; 5 | 6 | export function deserializeFastfillerPlan(serialized: string) { 7 | const plan: Partial = { 8 | extensions: [], 9 | spawns: [], 10 | containers: [], 11 | roads: [], 12 | link: undefined 13 | }; 14 | for (const s of deserializePlannedStructures(serialized)) { 15 | if (isPlannedStructure(STRUCTURE_EXTENSION)(s)) { 16 | plan.extensions?.push(s); 17 | } else if (isPlannedStructure(STRUCTURE_ROAD)(s)) { 18 | plan.roads?.push(s); 19 | } else if (isPlannedStructure(STRUCTURE_SPAWN)(s)) { 20 | plan.spawns?.push(s); 21 | } else if (isPlannedStructure(STRUCTURE_CONTAINER)(s)) { 22 | plan.containers?.push(s); 23 | } else if (isPlannedStructure(STRUCTURE_LINK)(s)) { 24 | plan.link = s; 25 | } 26 | } 27 | return validateFastfillerPlan(plan); 28 | } 29 | -------------------------------------------------------------------------------- /src/RoomPlanner/Stamps/deserializeHeadquartersPlan.ts: -------------------------------------------------------------------------------- 1 | import type { HeadquartersPlan } from 'RoomPlanner'; 2 | import { deserializePlannedStructures } from 'Selectors/plannedStructures'; 3 | import { isPlannedStructure } from 'Selectors/typeguards'; 4 | import { validateHeadquartersPlan } from './validateHeadquartersPlan'; 5 | 6 | export function deserializeHeadquartersPlan(serialized: string) { 7 | const plan: Partial = { 8 | link: undefined, 9 | factory: undefined, 10 | storage: undefined, 11 | terminal: undefined, 12 | nuker: undefined, 13 | roads: [], 14 | extension: undefined 15 | }; 16 | for (const s of deserializePlannedStructures(serialized)) { 17 | if (isPlannedStructure(STRUCTURE_NUKER)(s)) plan.nuker = s; 18 | if (isPlannedStructure(STRUCTURE_POWER_SPAWN)(s)) plan.powerSpawn = s; 19 | if (isPlannedStructure(STRUCTURE_LINK)(s)) plan.link = s; 20 | if (isPlannedStructure(STRUCTURE_FACTORY)(s)) plan.factory = s; 21 | if (isPlannedStructure(STRUCTURE_STORAGE)(s)) plan.storage = s; 22 | if (isPlannedStructure(STRUCTURE_TERMINAL)(s)) plan.terminal = s; 23 | if (isPlannedStructure(STRUCTURE_EXTENSION)(s)) plan.extension = s; 24 | if (isPlannedStructure(STRUCTURE_ROAD)(s)) plan.roads?.push(s); 25 | } 26 | return validateHeadquartersPlan(plan); 27 | } 28 | -------------------------------------------------------------------------------- /src/RoomPlanner/Stamps/deserializeLabsPlan.ts: -------------------------------------------------------------------------------- 1 | import type { LabsPlan } from 'RoomPlanner'; 2 | import { deserializePlannedStructures } from 'Selectors/plannedStructures'; 3 | import { isPlannedStructure } from 'Selectors/typeguards'; 4 | import { validateLabsPlan } from './validateLabsPlan'; 5 | 6 | export function deserializeLabsPlan(serialized: string) { 7 | const plan: LabsPlan = { 8 | labs: [], 9 | roads: [] 10 | }; 11 | for (const s of deserializePlannedStructures(serialized)) { 12 | if (isPlannedStructure(STRUCTURE_LAB)(s)) { 13 | plan.labs.push(s); 14 | } else if (isPlannedStructure(STRUCTURE_ROAD)(s)) { 15 | plan.roads.push(s); 16 | } 17 | } 18 | return validateLabsPlan(plan); 19 | } 20 | -------------------------------------------------------------------------------- /src/RoomPlanner/Stamps/fitStamp.ts: -------------------------------------------------------------------------------- 1 | import { diamondDistanceTransform, distanceTransform } from 'RoomPlanner/Algorithms/distancetransform'; 2 | import { forEverySquareInRoom } from 'Selectors/Map/MapCoordinates'; 3 | import { Coord } from 'utils/packrat'; 4 | import { Stamp } from './stamps'; 5 | 6 | export function fitStamp(room: string, stamp: Stamp, costMatrix?: CostMatrix, diamond = false) { 7 | // pin stamp if initial spawn 8 | if (stamp.some(s => s.includes(STRUCTURE_SPAWN))) { 9 | const initialSpawn = Game.rooms[room]?.find(FIND_MY_SPAWNS)[0]; 10 | if (initialSpawn) { 11 | for (let y = 0; y < stamp.length; y++) { 12 | for (let x = 0; x < stamp[y].length; x++) { 13 | if (stamp[y][x] === STRUCTURE_SPAWN) { 14 | return [{ x: initialSpawn.pos.x - x, y: initialSpawn.pos.y - y }]; 15 | } 16 | } 17 | } 18 | } 19 | } 20 | 21 | const minMargin = 3; // do not put stamps closer than 3 squares to the edge of the room 22 | const dt = (diamond ? diamondDistanceTransform : distanceTransform)(room, false, costMatrix); 23 | const squares: Coord[] = []; 24 | const offset = Math.floor(stamp.length / 2); 25 | forEverySquareInRoom((x, y) => { 26 | const topLeft = { x: x - offset, y: y - offset }; 27 | const bottomRight = { x: x + offset, y: y + offset }; 28 | if ( 29 | topLeft.x <= minMargin || 30 | topLeft.y <= minMargin || 31 | bottomRight.x + minMargin >= 50 || 32 | bottomRight.y + minMargin >= 50 33 | ) 34 | return; 35 | 36 | if (x > offset && y > offset && x + offset < 50 && y + offset < 50 && dt.get(x, y) > offset) { 37 | squares.push({ x: x - offset, y: y - offset }); 38 | } 39 | }); 40 | return squares; 41 | } 42 | 43 | export function applyStamp(stamp: Stamp, topLeft: Coord, costMatrix: CostMatrix) { 44 | const cm = costMatrix.clone(); 45 | stamp.forEach((row, y) => { 46 | row.forEach((cell, x) => { 47 | cm.set( 48 | topLeft.x + x, 49 | topLeft.y + y, 50 | Math.max(cell === undefined || cell === STRUCTURE_ROAD ? 0 : 255, cm.get(topLeft.x + x, topLeft.y + y)) 51 | ); 52 | }); 53 | }); 54 | return cm; 55 | } 56 | -------------------------------------------------------------------------------- /src/RoomPlanner/Stamps/pointsOfInterest.ts: -------------------------------------------------------------------------------- 1 | import { flowfields } from 'RoomPlanner/Algorithms/flowfield'; 2 | import { controllerPosition, mineralPosition, roomExits, sourcePositions } from 'Selectors/roomCache'; 3 | import { memoize } from 'utils/memoizeFunction'; 4 | 5 | const avoidBorders = () => { 6 | const borders = [0, 1, 48, 49]; 7 | const cm = new PathFinder.CostMatrix(); 8 | for (const x of borders) { 9 | for (let y = 0; y < 50; y++) { 10 | cm.set(x, y, 255); 11 | } 12 | } 13 | for (const y of borders) { 14 | for (let x = 2; x < 48; x++) { 15 | cm.set(x, y, 255); 16 | } 17 | } 18 | return cm; 19 | }; 20 | 21 | export const pointsOfInterest = memoize( 22 | room => room, 23 | (room: string) => { 24 | const controller = controllerPosition(room); 25 | const [source1, source2] = sourcePositions(room); 26 | const mineral = mineralPosition(room); 27 | const exits = roomExits(room); 28 | if (!controller || !source1 || !source2 || !mineral || !exits) { 29 | // console.log( 30 | // 'controller', 31 | // !controller, 32 | // 'source1', 33 | // !source1, 34 | // 'source2', 35 | // !source2, 36 | // 'mineral', 37 | // !mineral, 38 | // 'exits', 39 | // !exits 40 | // ); 41 | throw new Error('Unable to generate flowfields for room'); 42 | } 43 | 44 | const borders = avoidBorders(); 45 | 46 | // paths to exits can ignore the border restriction; no other paths should 47 | // be closer than 2 squares to the border 48 | return { 49 | ...flowfields( 50 | room, 51 | { 52 | controller: [controller], 53 | source1: [source1], 54 | source2: [source2], 55 | mineral: [mineral] 56 | }, 57 | borders 58 | ), 59 | ...flowfields(room, { 60 | exits 61 | }) 62 | }; 63 | } 64 | ); 65 | -------------------------------------------------------------------------------- /src/RoomPlanner/Stamps/validateFastfillerPlan.ts: -------------------------------------------------------------------------------- 1 | import { FastfillerPlan } from 'RoomPlanner'; 2 | 3 | export function validateFastfillerPlan(plan: Partial) { 4 | if ( 5 | plan.extensions?.length !== 15 || 6 | plan.spawns?.length !== 3 || 7 | plan.containers?.length !== 2 || 8 | !plan.roads?.length || 9 | !plan.link 10 | ) { 11 | throw new Error(`Incomplete FastfillerPlan`); 12 | } else { 13 | return plan as FastfillerPlan; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/RoomPlanner/Stamps/validateHeadquartersPlan.ts: -------------------------------------------------------------------------------- 1 | import { HeadquartersPlan } from 'RoomPlanner'; 2 | 3 | export function validateHeadquartersPlan(plan: Partial) { 4 | if ( 5 | !plan.link || 6 | !plan.factory || 7 | !plan.storage || 8 | !plan.terminal || 9 | !plan.powerSpawn || 10 | !plan.extension || 11 | !plan.nuker || 12 | !plan.roads?.length 13 | ) { 14 | // console.log(JSON.stringify(plan, null, 2)) 15 | throw new Error(`Incomplete HeadquartersPlan`); 16 | } else { 17 | return plan as HeadquartersPlan; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/RoomPlanner/Stamps/validateLabsPlan.ts: -------------------------------------------------------------------------------- 1 | import { LabsPlan } from "RoomPlanner"; 2 | 3 | 4 | export function validateLabsPlan(plan: Partial) { 5 | if (plan.labs?.length !== 10 || !plan.roads?.length) { 6 | throw new Error(`Incomplete LabsPlan`); 7 | } else { 8 | return plan as LabsPlan; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/RoomPlanner/planRooms.ts: -------------------------------------------------------------------------------- 1 | import profiler from "utils/profiler"; 2 | import { generateRoomPlans } from "./RoomArchitect"; 3 | 4 | export const planRooms = profiler.registerFN(() => { 5 | let start = Game.cpu.getUsed(); 6 | if (Game.cpu.bucket < 500) return; // Don't do room planning at low bucket levels 7 | 8 | Memory.roomPlans ??= {}; 9 | 10 | for (let room in Memory.rooms) { 11 | if (Memory.roomPlans[room]?.complete) continue; // Already planned 12 | if (!Memory.rooms[room].controllerId) continue; // No controller or room hasn't been properly scanned yet 13 | if (Game.cpu.getUsed() - start <= 5) { 14 | generateRoomPlans(room); 15 | } 16 | } 17 | }, 'planRooms') 18 | -------------------------------------------------------------------------------- /src/RoomPlanner/scanRoomPlanStructures.ts: -------------------------------------------------------------------------------- 1 | import { ScannedRoomEvent } from 'Intel/events'; 2 | import { plannedActiveFranchiseRoads } from 'Selectors/plannedActiveFranchiseRoads'; 3 | import { plannedStructuresByRcl } from 'Selectors/plannedStructuresByRcl'; 4 | 5 | let scanned: Record = {}; 6 | 7 | export const scanRoomPlanStructures = ({ room, office }: ScannedRoomEvent) => { 8 | if (!office) return; 9 | const structures = Game.rooms[room]?.find(FIND_STRUCTURES).length; 10 | const rcl = Game.rooms[room]?.controller?.level ?? 0; 11 | if (Game.rooms[room] && (scanned[room] !== structures || Game.time % 50 === 0)) { 12 | scanned[room] = structures; 13 | for (let s of plannedStructuresByRcl(room, 8)) { 14 | s.survey(); 15 | } 16 | for (let s of plannedActiveFranchiseRoads(room)) { 17 | s.survey(); 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/Selectors/Combat/defenseRamparts.ts: -------------------------------------------------------------------------------- 1 | import { BARRIER_LEVEL } from 'config'; 2 | import { findHostileCreeps, findHostileCreepsInRange } from 'Selectors/findHostileCreeps'; 3 | import { rcl } from 'Selectors/rcl'; 4 | import { roomPlans } from 'Selectors/roomPlans'; 5 | import { repairThreshold } from 'Selectors/Structures/repairThreshold'; 6 | import { memoizeByTick } from 'utils/memoizeFunction'; 7 | 8 | /** 9 | * Return the ramparts that are close to an enemy 10 | */ 11 | export const rampartsToDefend = memoizeByTick( 12 | room => room, 13 | (room: string) => { 14 | if (!findHostileCreeps(room).length) return []; 15 | const ramparts = roomPlans(room)?.perimeter?.ramparts ?? []; 16 | if (ramparts.length === 0) return []; 17 | return ramparts.filter(r => r.structure && findHostileCreepsInRange(r.pos, 5).length); 18 | } 19 | ); 20 | 21 | /** 22 | * True if ramparts have lost containment 23 | */ 24 | export const rampartsAreBroken = memoizeByTick( 25 | room => room, 26 | (room: string) => { 27 | return Boolean(roomPlans(room)?.perimeter?.ramparts.some(r => !r.structure)); 28 | } 29 | ); 30 | 31 | /** 32 | * Returns ramparts in need of repair, in priority order 33 | */ 34 | export const rampartsToRepair = memoizeByTick( 35 | room => room, 36 | (room: string) => { 37 | const ramparts = roomPlans(room)?.perimeter?.ramparts ?? []; 38 | return ramparts 39 | .filter(r => !r.structure || BARRIER_LEVEL[rcl(room)] * repairThreshold(r) > r.structure.hits) 40 | .sort((a, b) => (a.structure?.hits ?? 0) - (b.structure?.hits ?? 0)); 41 | } 42 | ); 43 | -------------------------------------------------------------------------------- /src/Selectors/Combat/priorityTarget.ts: -------------------------------------------------------------------------------- 1 | import { findHostileCreeps } from 'Selectors/findHostileCreeps'; 2 | import { myDamageNet } from './costMatrixes'; 3 | 4 | export const priorityKillTarget = (room: string) => { 5 | if (!Game.rooms[room]) return; 6 | const hostiles = findHostileCreeps(room); 7 | if (!hostiles.length) return; 8 | return hostiles.reduce((a, b) => (myDamageNet(a.pos) > myDamageNet(b.pos) ? a : b)); 9 | }; 10 | -------------------------------------------------------------------------------- /src/Selectors/Combat/shouldPlunder.ts: -------------------------------------------------------------------------------- 1 | import { buyMarketPrice } from 'Selectors/Market/marketPrice'; 2 | 3 | export const resourcesToPlunder = (distance: number, lootResources: ResourceConstant[]) => { 4 | let trips = CREEP_LIFE_TIME / distance; 5 | // cost of a single M/C segment, divided by the amount of resources it can move from this room 6 | const moveCost = (BODYPART_COST[CARRY] + BODYPART_COST[MOVE]) / Math.floor(CARRY_CAPACITY * trips); 7 | 8 | return lootResources.filter(resource => moveCost > buyMarketPrice(resource)); 9 | }; 10 | -------------------------------------------------------------------------------- /src/Selectors/Combat/towerDamage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Possible tower damage from a specific tower at a given square 3 | */ 4 | export const towerDamage = (tower: RoomPosition, target: RoomPosition) => { 5 | 6 | } 7 | 8 | /** 9 | * Total possible tower damage from all towers at a given square 10 | */ 11 | export const allTowersDamage = (room: string, target: RoomPosition) => { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/Selectors/Franchises/franchiseActive.ts: -------------------------------------------------------------------------------- 1 | import { posById } from '../posById'; 2 | import { franchisesByOffice } from './franchisesByOffice'; 3 | 4 | export const franchiseActive = (office: string, source: Id, sinceTicks = 1500) => { 5 | const room = posById(source)?.roomName ?? ''; 6 | const lastHarvested = Memory.offices[office]?.franchises[source]?.lastActive; 7 | return lastHarvested && lastHarvested + sinceTicks >= Game.time; 8 | }; 9 | 10 | export const activeFranchises = (office: string, sinceTicks = 1500) => 11 | franchisesByOffice(office).filter(source => franchiseActive(office, source.source, sinceTicks)); 12 | -------------------------------------------------------------------------------- /src/Selectors/Franchises/franchiseDefenseRooms.ts: -------------------------------------------------------------------------------- 1 | import { getCachedPath } from 'screeps-cartographer'; 2 | import { memoize } from 'utils/memoizeFunction'; 3 | 4 | export const franchiseDefenseRooms = memoize( 5 | (office: string, franchise: Id) => `${getCachedPath(office + franchise)?.length}`, 6 | (office: string, franchise: Id) => { 7 | return ( 8 | getCachedPath(office + franchise)?.reduce((rooms, road) => { 9 | if (!rooms.includes(road.roomName)) rooms.push(road.roomName); 10 | return rooms; 11 | }, [] as string[]) ?? [] 12 | ); 13 | } 14 | ); 15 | -------------------------------------------------------------------------------- /src/Selectors/Franchises/franchiseDisabled.ts: -------------------------------------------------------------------------------- 1 | import { FRANCHISE_EVALUATE_PERIOD } from 'config'; 2 | import { posById } from 'Selectors/posById'; 3 | import { memoizeByTick } from 'utils/memoizeFunction'; 4 | import { getFranchiseDistance } from './getFranchiseDistance'; 5 | 6 | export const franchiseDisabled = memoizeByTick( 7 | (office: string, source: Id) => office + source, 8 | (office, source) => { 9 | const pos = posById(source); 10 | if (!pos || !Memory.offices[office]?.franchises[source]) return true; 11 | if ((getFranchiseDistance(office, source) ?? 200) > 150) return true; 12 | const { scores } = Memory.offices[office].franchises[source]; 13 | if (scores.length === FRANCHISE_EVALUATE_PERIOD && scores.reduce((a, b) => a + b, 0) / scores.length < 0) { 14 | // franchise is too expensive 15 | return true; 16 | } 17 | return false; 18 | } 19 | ); 20 | -------------------------------------------------------------------------------- /src/Selectors/Franchises/franchiseEnergyAvailable.ts: -------------------------------------------------------------------------------- 1 | import { memoizeByTick } from 'utils/memoizeFunction'; 2 | import { posById } from '../posById'; 3 | import { resourcesNearPos } from '../resourcesNearPos'; 4 | import { getFranchisePlanBySourceId } from '../roomPlans'; 5 | 6 | const franchiseEnergyCache = new Map, number>(); 7 | 8 | export const franchiseEnergyAvailable = memoizeByTick( 9 | source => source, 10 | (source: Id) => { 11 | const pos = posById(source); 12 | if (!pos) return 0; 13 | 14 | if (!Game.rooms[pos.roomName]) return franchiseEnergyCache.get(source) ?? 0; 15 | 16 | const container = getFranchisePlanBySourceId(source)?.container.structure as StructureContainer | undefined; 17 | 18 | let amount = 19 | resourcesNearPos(pos, 1, RESOURCE_ENERGY).reduce((sum, r) => sum + r.amount, 0) + 20 | (container?.store.getUsedCapacity(RESOURCE_ENERGY) ?? 0); 21 | franchiseEnergyCache.set(source, amount); 22 | 23 | return amount; 24 | } 25 | ); 26 | -------------------------------------------------------------------------------- /src/Selectors/Franchises/franchiseIncome.ts: -------------------------------------------------------------------------------- 1 | import { HarvestMission } from 'Missions/Implementations/HarvestMission'; 2 | import { activeMissions, isMission } from 'Missions/Selectors'; 3 | import { sum } from 'Selectors/reducers'; 4 | import { memoize } from 'utils/memoizeFunction'; 5 | 6 | export const franchiseIncome = (office: string) => { 7 | return activeMissions(office) 8 | .filter(isMission(HarvestMission)) 9 | .map(m => m.harvestRate()) 10 | .reduce(sum, 0); 11 | }; 12 | 13 | export const franchiseCapacity = memoize( // cached every 10 ticks 14 | office => office, 15 | (office: string) => { 16 | return activeMissions(office) 17 | .filter(isMission(HarvestMission)) 18 | .map(m => m.haulingCapacityNeeded()) 19 | .reduce(sum, 0); 20 | }, 21 | 10 22 | ) 23 | -------------------------------------------------------------------------------- /src/Selectors/Franchises/franchiseIsFull.ts: -------------------------------------------------------------------------------- 1 | import { HarvestMission } from 'Missions/Implementations/HarvestMission'; 2 | import { activeMissions, isMission } from 'Missions/Selectors'; 3 | import { adjacentWalkablePositions } from 'Selectors/Map/MapCoordinates'; 4 | import { sum } from 'Selectors/reducers'; 5 | import { memoizeByTick } from 'utils/memoizeFunction'; 6 | import { posById } from '../posById'; 7 | 8 | export const franchiseIsFull = memoizeByTick( 9 | (office, id) => office + id, 10 | (office: string, id: Id) => { 11 | const pos = posById(id); 12 | const harvestRate = activeMissions(office) 13 | .filter(isMission(HarvestMission)) 14 | .filter(m => m.missionData.source === id) 15 | .map(m => m.harvestRate()) 16 | .reduce(sum, 0); 17 | if (id && harvestRate >= 10) return true; 18 | if (!pos || !Game.rooms[pos.roomName]) return false; // Can't find the source, don't know if it's full 19 | 20 | return adjacentWalkablePositions(pos, false).length === 0; 21 | } 22 | ); 23 | -------------------------------------------------------------------------------- /src/Selectors/Franchises/franchiseThatNeedsEngineers.ts: -------------------------------------------------------------------------------- 1 | import { EngineerMission } from 'Missions/OldImplementations/Engineer'; 2 | import { assignedCreep } from 'Missions/Selectors'; 3 | import { adjustedPlannedFranchiseRoadsCost } from '../plannedFranchiseRoads'; 4 | import { rcl } from '../rcl'; 5 | import { franchisesThatNeedRoadWork } from './franchisesThatNeedRoadWork'; 6 | 7 | export const franchiseThatNeedsEngineers = (office: string, missions: EngineerMission[], includeFull = false) => { 8 | if (rcl(office) < 3) return undefined; 9 | const remotes = franchisesThatNeedRoadWork(office); 10 | if (!remotes.length) return undefined; 11 | 12 | const assignedCapacity = missions.reduce((sum, m) => { 13 | if (m.data.franchise) 14 | sum[m.data.franchise] = 15 | (sum[m.data.franchise] ?? 0) + m.data.workParts * (assignedCreep(m)?.ticksToLive ?? CREEP_LIFE_TIME); 16 | return sum; 17 | }, , number>>{}); 18 | 19 | let min, minAssigned; 20 | for (const source of remotes) { 21 | // For active missions, I want to get the franchise with the least work assigned; for new 22 | // missions, I want to return nothing if there is already enough capacity assigned 23 | const workAssigned = assignedCapacity[source] ?? 0; 24 | const costRemaining = adjustedPlannedFranchiseRoadsCost(office, source) - workAssigned; 25 | if (!includeFull && costRemaining <= 0) continue; 26 | 27 | if (!min || minAssigned === undefined || workAssigned < minAssigned) { 28 | min = source; 29 | minAssigned = workAssigned; 30 | } 31 | } 32 | 33 | return min; 34 | }; 35 | -------------------------------------------------------------------------------- /src/Selectors/Franchises/franchisesByOffice.ts: -------------------------------------------------------------------------------- 1 | import { sourceIds } from '../roomCache'; 2 | import { remoteFranchises } from './remoteFranchises'; 3 | 4 | export const franchisesByOffice = (officeName: string) => { 5 | return sourceIds(officeName) 6 | .map(source => ({ 7 | source, 8 | room: officeName, 9 | remote: false 10 | })) 11 | .concat(remoteFranchises(officeName).map(f => ({ ...f, remote: true }))); 12 | }; 13 | -------------------------------------------------------------------------------- /src/Selectors/Franchises/franchisesThatNeedRoadWork.ts: -------------------------------------------------------------------------------- 1 | import { nextFranchiseRoadToBuild } from '../plannedFranchiseRoads'; 2 | import { activeFranchises } from './franchiseActive'; 3 | 4 | export function franchisesThatNeedRoadWork(office: string) { 5 | return activeFranchises(office) 6 | .filter(({ remote, source, room }) => { 7 | return ( 8 | remote && 9 | (Memory.offices[office]?.franchises[source]?.lastActive ?? 0) + 1500 > Game.time && 10 | nextFranchiseRoadToBuild(office, source) 11 | ); 12 | }) 13 | .map(({ source }) => source); 14 | } 15 | -------------------------------------------------------------------------------- /src/Selectors/Franchises/furthestActiveFranchiseRoundTripDistance.ts: -------------------------------------------------------------------------------- 1 | import { sum } from 'Selectors/reducers'; 2 | import { memoizeByTick } from 'utils/memoizeFunction'; 3 | import { activeFranchises } from './franchiseActive'; 4 | import { getFranchiseDistance } from './getFranchiseDistance'; 5 | 6 | export const furthestActiveFranchiseRoundTripDistance = memoizeByTick( 7 | office => office, 8 | (office: string) => { 9 | return ( 10 | Math.max(10, ...activeFranchises(office).map(franchise => getFranchiseDistance(office, franchise.source) ?? 0)) * 11 | 2 12 | ); 13 | } 14 | ); 15 | 16 | export const averageActiveFranchiseRoundTripDistance = memoizeByTick( 17 | office => office, 18 | (office: string) => { 19 | const total = activeFranchises(office) 20 | .map(franchise => getFranchiseDistance(office, franchise.source) ?? 0) 21 | .reduce(sum, 0); 22 | const average = (Math.max(10, total) * 2) / activeFranchises(office).length; 23 | return average; 24 | } 25 | ); 26 | -------------------------------------------------------------------------------- /src/Selectors/Franchises/getFranchiseDistance.ts: -------------------------------------------------------------------------------- 1 | import { terrainCostAt } from 'Selectors/Map/MapCoordinates'; 2 | import { plannedFranchiseRoads } from 'Selectors/plannedFranchiseRoads'; 3 | import { posById } from 'Selectors/posById'; 4 | import { memoize } from 'utils/memoizeFunction'; 5 | 6 | export const getFranchiseDistance = memoize( 7 | (office: string, sourceId: Id) => office + posById(sourceId) + plannedFranchiseRoads(office, sourceId).length, 8 | (office: string, sourceId: Id) => { 9 | const roads = plannedFranchiseRoads(office, sourceId); 10 | if (!roads.length) return undefined; 11 | 12 | let cost = 0; 13 | for (const road of roads) { 14 | cost += road.structureId ? 1 : terrainCostAt(road.pos); 15 | } 16 | return cost; 17 | }, 18 | 200 19 | ); 20 | -------------------------------------------------------------------------------- /src/Selectors/Franchises/harvestMissionsByOffice.ts: -------------------------------------------------------------------------------- 1 | import { HarvestMission } from 'Missions/Implementations/HarvestMission'; 2 | import { isMission, missionsByOffice } from 'Missions/Selectors'; 3 | 4 | export const harvestMissionsByOffice = (officeName: string) => { 5 | return missionsByOffice()[officeName].filter(isMission(HarvestMission)); 6 | }; 7 | -------------------------------------------------------------------------------- /src/Selectors/Franchises/planFranchisePath.ts: -------------------------------------------------------------------------------- 1 | import { cachePath } from 'screeps-cartographer'; 2 | import { getCostMatrix } from 'Selectors/Map/Pathing'; 3 | import { getFranchisePlanBySourceId, roomPlans } from 'Selectors/roomPlans'; 4 | import { getTerritoryIntent, TerritoryIntent } from 'Selectors/territoryIntent'; 5 | 6 | export function planFranchisePath(office: string, source: Id) { 7 | const storage = roomPlans(office)?.headquarters?.storage.pos; 8 | const harvestPos = getFranchisePlanBySourceId(source)?.container.pos; 9 | if (!storage || !harvestPos) return []; 10 | return ( 11 | cachePath( 12 | office + source, 13 | storage, 14 | { pos: harvestPos, range: 1 }, 15 | { 16 | roomCallback: (room: string) => { 17 | if (getTerritoryIntent(room) === TerritoryIntent.AVOID) return false; 18 | return getCostMatrix(room, false, { territoryPlannedRoadsCost: 1, roomPlan: Boolean(Memory.offices[room]) }); 19 | }, 20 | plainCost: 2, 21 | swampCost: 2, 22 | roadCost: 1, 23 | maxOps: 100000, 24 | reusePath: 100000 25 | } 26 | ) ?? [] 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/Selectors/Franchises/remoteFranchises.ts: -------------------------------------------------------------------------------- 1 | import { getTerritoriesByOffice } from 'Selectors/getTerritoriesByOffice'; 2 | import { sourceIds } from 'Selectors/roomCache'; 3 | 4 | export const remoteFranchises = (office: string) => { 5 | const territories = getTerritoriesByOffice(office); 6 | return territories.flatMap(room => sourceIds(room).map(source => ({ source, room }))); 7 | }; 8 | -------------------------------------------------------------------------------- /src/Selectors/Logistics/predictiveCapacity.ts: -------------------------------------------------------------------------------- 1 | import { memoizeOncePerTick } from "utils/memoizeFunction"; 2 | 3 | const energyCache = memoizeOncePerTick(() => new Map, number>()); 4 | 5 | export const estimatedUsedCapacity = (target?: Creep|AnyStoreStructure|Tombstone) => { 6 | if (!target) return 0; 7 | const cache = energyCache(); 8 | if (!cache.has(target.id)) { 9 | cache.set(target.id, target.store.getUsedCapacity(RESOURCE_ENERGY)); 10 | } 11 | return cache.get(target.id) ?? 0; 12 | } 13 | export const updateUsedCapacity = (target: Creep|AnyStoreStructure|Tombstone, delta: number) => { 14 | const cache = energyCache(); 15 | cache.set(target.id, Math.max(0, Math.min(target.store.getCapacity(RESOURCE_ENERGY) ?? 0, (cache.get(target.id) ?? 0) + delta))); 16 | } 17 | export const estimatedFreeCapacity = (target?: Creep|AnyStoreStructure|Tombstone) => { 18 | if (!target) return 0; 19 | return Math.max(0, 20 | (target.store.getCapacity(RESOURCE_ENERGY) ?? 0) - estimatedUsedCapacity(target) 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/Selectors/Map/getRoomPathDistance.ts: -------------------------------------------------------------------------------- 1 | import { getTerritoryIntent, TerritoryIntent } from 'Selectors/territoryIntent'; 2 | import { memoize } from 'utils/memoizeFunction'; 3 | 4 | export const getRoomPathDistance = memoize( 5 | (room1: string, room2: string) => [room1, room2].sort().join(''), 6 | (room1: string, room2: string) => { 7 | const newRoute = Game.map.findRoute(room1, room2, { 8 | routeCallback: room => (getTerritoryIntent(room) === TerritoryIntent.AVOID ? Infinity : 0) 9 | }); 10 | if (newRoute === -2) return undefined; 11 | return newRoute.length; 12 | } 13 | ); 14 | -------------------------------------------------------------------------------- /src/Selectors/Market/boostCost.ts: -------------------------------------------------------------------------------- 1 | import { RESOURCE_INGREDIENTS } from 'gameConstants'; 2 | import { buyMarketEnergyPrice } from './marketPrice'; 3 | 4 | export const boostCost = (boost: MineralCompoundConstant): number => { 5 | let buyPrice = buyMarketEnergyPrice(boost); 6 | // get cost of ingredients 7 | if (!RESOURCE_INGREDIENTS[boost]) { 8 | return buyPrice; // no ingredients to make it ourselves 9 | } 10 | const [ingredient1, ingredient2] = RESOURCE_INGREDIENTS[boost]; 11 | return Math.min( 12 | buyPrice, 13 | boostCost(ingredient1 as MineralCompoundConstant) + boostCost(ingredient2 as MineralCompoundConstant) 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/Selectors/Missions/missionEnergyAvailable.ts: -------------------------------------------------------------------------------- 1 | export const MissionEnergyAvailable: Record = {}; 2 | -------------------------------------------------------------------------------- /src/Selectors/Missions/updateMissionEnergyAvailable.ts: -------------------------------------------------------------------------------- 1 | import { activeMissions } from 'Missions/Selectors'; 2 | import { energyInProduction } from 'Selectors/energyInProduction'; 3 | import { roomEnergyAvailable } from 'Selectors/storageEnergyAvailable'; 4 | import { MissionEnergyAvailable } from './missionEnergyAvailable'; 5 | 6 | export const updateMissionEnergyAvailable = () => { 7 | // run every 3 ticks 8 | if (Game.time % 3) return; 9 | for (const office in Memory.offices) { 10 | let energy = 11 | Math.max(roomEnergyAvailable(office), energyInProduction(office)) - 12 | (Game.rooms[office].energyCapacityAvailable - Game.rooms[office].energyAvailable); 13 | for (const mission of activeMissions(office)) { 14 | energy -= mission.energyRemaining(); 15 | } 16 | MissionEnergyAvailable[office] = energy; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/Selectors/Structures/destroyUnplannedStructures.ts: -------------------------------------------------------------------------------- 1 | export const destroyUnplannedStructures = (room: string) => { 2 | if (!Game.rooms[room]?.controller?.my || !Memory.roomPlans?.[room]?.office) return; 3 | // Destroy all controller-limited structures 4 | Game.rooms[room].find(FIND_STRUCTURES).forEach(s => { 5 | if (s.structureType !== STRUCTURE_ROAD && s.structureType !== STRUCTURE_CONTROLLER) { 6 | s.destroy(); 7 | } 8 | }); 9 | Game.rooms[room].find(FIND_CONSTRUCTION_SITES).forEach(s => s.remove()); 10 | }; 11 | -------------------------------------------------------------------------------- /src/Selectors/Structures/repairThreshold.ts: -------------------------------------------------------------------------------- 1 | import { PlannedStructure } from 'RoomPlanner/PlannedStructure'; 2 | 3 | /** 4 | * If structure is missing more than this many hitpoints, 5 | * repair it 6 | */ 7 | export const repairThreshold = (structure: PlannedStructure) => { 8 | const barrierThresholdTypes = [STRUCTURE_RAMPART, STRUCTURE_CONTAINER, STRUCTURE_WALL] as string[]; 9 | if (structure.structureType === STRUCTURE_ROAD) { 10 | return (structure.structure?.hitsMax ?? ROAD_HITS) * 0.5; 11 | } else if (barrierThresholdTypes.includes(structure.structureType)) { 12 | return CARRY_CAPACITY * REPAIR_POWER * 0.1; // 10% of a carry's worth of repair hits 13 | } else { 14 | return 0; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/Selectors/boostQuotas.ts: -------------------------------------------------------------------------------- 1 | import { BOOSTS_BY_INTENT } from 'gameConstants'; 2 | 3 | export const boostQuotas = (office: string) => { 4 | return [ 5 | ...BOOSTS_BY_INTENT.TOUGH, 6 | ...BOOSTS_BY_INTENT.ATTACK, 7 | ...BOOSTS_BY_INTENT.MOVE, 8 | ...BOOSTS_BY_INTENT.HEAL, 9 | ...BOOSTS_BY_INTENT.HARVEST, 10 | ...BOOSTS_BY_INTENT.UPGRADE, 11 | ...BOOSTS_BY_INTENT.RANGED_ATTACK 12 | ] 13 | .map(boost => ({ 14 | boost, 15 | amount: 30 * 50 16 | })) 17 | .sort((a, b) => a.boost.length - b.boost.length); // sort T1 boosts to the front of the queue 18 | }; 19 | -------------------------------------------------------------------------------- /src/Selectors/boostsToSupply.ts: -------------------------------------------------------------------------------- 1 | import { byId } from "./byId"; 2 | import { getLabs } from "./getLabs"; 3 | 4 | export function boostsToSupply(office: string) { 5 | const boostOrders = Memory.offices[office].lab.boosts; 6 | const labs = getLabs(office); 7 | 8 | const resources = new Map(); 9 | 10 | for (const order of boostOrders) { 11 | // Subtract any already-boosted parts from the orders 12 | const c = byId(order.id); 13 | const orderResources = order.boosts.reduce((map, boost) => { 14 | map.set(boost.type, map.get(boost.type) ?? 0 + boost.count); 15 | return map; 16 | }, new Map()) 17 | if (!c) continue; 18 | c.body.forEach(part => { 19 | const amount = orderResources.get(part.boost as ResourceConstant) 20 | if (amount) { 21 | orderResources.set(part.boost as ResourceConstant, amount - 1) 22 | } 23 | }) 24 | 25 | // Add remaining resources to quotas 26 | for (const [resource, amount] of orderResources.entries()) { 27 | resources.set(resource, (resources.get(resource) ?? 0) + amount); 28 | } 29 | } 30 | 31 | for (const boostLab of labs.boosts) { 32 | const lab = boostLab.structure as StructureLab|undefined; 33 | const resource = Object.keys(lab?.store ?? {})[0] as ResourceConstant|undefined; 34 | if (!lab || !resource) continue; 35 | 36 | const amount = lab.store.getUsedCapacity(resource); 37 | if (!resources.has(resource)) return true; // Should empty unneeded boosts 38 | if (amount && amount < resources.get(resource)!) return true; // Need more boosts to fulfill orders 39 | resources.delete(resource); // Sufficient capacity to meet quota 40 | } 41 | 42 | return resources; // Resources that need to be stocked 43 | } 44 | -------------------------------------------------------------------------------- /src/Selectors/byId.ts: -------------------------------------------------------------------------------- 1 | export function byId(id: Id|undefined) { 2 | return id ? Game.getObjectById(id) ?? undefined : undefined 3 | } 4 | -------------------------------------------------------------------------------- /src/Selectors/carryPartsForFranchiseRoute.ts: -------------------------------------------------------------------------------- 1 | export const carryPartsForFranchiseRoute = (roomIsOwned: boolean, routeLength: number) => { 2 | const roundTripDistance = routeLength * 1.5; 3 | let energyCapacity = (roomIsOwned) ? SOURCE_ENERGY_CAPACITY : SOURCE_ENERGY_NEUTRAL_CAPACITY; 4 | const targetCapacity = (energyCapacity / ENERGY_REGEN_TIME) * roundTripDistance; 5 | return Math.ceil(targetCapacity / CARRY_CAPACITY); 6 | } 7 | -------------------------------------------------------------------------------- /src/Selectors/costMatrixFromRoomPlan.ts: -------------------------------------------------------------------------------- 1 | import { plannedOfficeStructuresByRcl } from "./plannedStructuresByRcl"; 2 | 3 | export const costMatrixFromRoomPlan = (room: string) => { 4 | const plan = new PathFinder.CostMatrix; 5 | for (const s of plannedOfficeStructuresByRcl(room, 8)) { 6 | if ((OBSTACLE_OBJECT_TYPES as string[]).includes(s.structureType)) { 7 | plan.set(s.pos.x, s.pos.y, 255) 8 | } 9 | } 10 | return plan; 11 | } 12 | -------------------------------------------------------------------------------- /src/Selectors/costToBoostMinion.ts: -------------------------------------------------------------------------------- 1 | import { FEATURES } from "config"; 2 | import { terminalBalance } from "./terminalBalance"; 3 | 4 | export function costToBoostMinion(office: string, parts: number, boost: MineralBoostConstant) { 5 | return FEATURES.LABS ? 6 | ((terminalBalance(office, boost) >= parts * 30) ? (parts * 20) : 0) / CREEP_LIFE_TIME : 7 | 0; 8 | } 9 | -------------------------------------------------------------------------------- /src/Selectors/cpuOverhead.ts: -------------------------------------------------------------------------------- 1 | import { CPU_ESTIMATE_PERIOD } from "config"; 2 | 3 | declare global { 4 | interface Memory { 5 | overhead: number; 6 | } 7 | } 8 | 9 | const overhead: number[] = new Array(CPU_ESTIMATE_PERIOD).fill(Memory.overhead ?? 0); 10 | 11 | let missionCpu = 0; 12 | export function recordMissionCpu(cpu: number) { 13 | missionCpu = cpu; 14 | } 15 | 16 | export function recordOverhead() { 17 | const cpuUsed = Game.cpu.getUsed(); 18 | overhead.push(Math.max(0, cpuUsed - missionCpu)); 19 | overhead.shift(); 20 | Memory.overhead = cpuOverhead(); 21 | } 22 | 23 | export function cpuOverhead() { 24 | return Game.cpu.limit * 0.4 // overhead.reduce((a, b) => a + b, 0) / overhead.length; 25 | } 26 | -------------------------------------------------------------------------------- /src/Selectors/creepCounter.ts: -------------------------------------------------------------------------------- 1 | export type CreepCount = Record>; 2 | 3 | let creeps: CreepCount = {}; 4 | 5 | export const resetCreepCount = () => { 6 | creeps = {}; 7 | } 8 | 9 | export const countCreep = (creep: Creep) => { 10 | creeps[creep.memory.office] ??= {} 11 | creeps[creep.memory.office][creep.memory.type] ??= 0 12 | creeps[creep.memory.office][creep.memory.type] += 1; 13 | } 14 | 15 | export const creepCount = () => creeps; 16 | -------------------------------------------------------------------------------- /src/Selectors/creepStats.ts: -------------------------------------------------------------------------------- 1 | export const creepStats = (creeps: Creep[]) => { 2 | return creeps.reduce((sum, creep) => { 3 | sum.count += 1; 4 | creep.body.forEach(p => { 5 | sum.hits += p.hits; 6 | if (p.hits) { 7 | if (p.type === HEAL) { 8 | let heal = HEAL_POWER; 9 | if (p.boost) { 10 | heal *= BOOSTS[HEAL][p.boost].heal; 11 | } 12 | sum.heal += heal; 13 | } else if (p.type === ATTACK) { 14 | let attack = ATTACK_POWER; 15 | if (p.boost) { 16 | attack *= BOOSTS[ATTACK][p.boost].attack; 17 | } 18 | sum.attack += attack; 19 | } else if (p.type === RANGED_ATTACK) { 20 | let rangedAttack = ATTACK_POWER; 21 | if (p.boost) { 22 | rangedAttack *= BOOSTS[RANGED_ATTACK][p.boost].rangedAttack; 23 | } 24 | sum.rangedAttack += rangedAttack; 25 | } 26 | } 27 | }) 28 | return sum; 29 | }, { count: 0, hits: 0, heal: 0, attack: 0, rangedAttack: 0 }) 30 | } 31 | -------------------------------------------------------------------------------- /src/Selectors/defaultDirectionsForSpawn.ts: -------------------------------------------------------------------------------- 1 | import { memoizeByTick } from "utils/memoizeFunction"; 2 | import { getTowerRefillerLocation } from "./getHqLocations"; 3 | import { roomPlans } from "./roomPlans"; 4 | 5 | export const defaultDirectionsForSpawn = memoizeByTick( 6 | (office: string, spawn: StructureSpawn) => office + spawn.id, 7 | (office: string, spawn: StructureSpawn) => { 8 | let directions = [TOP, TOP_RIGHT, RIGHT, BOTTOM_RIGHT, BOTTOM, BOTTOM_LEFT, LEFT, TOP_LEFT]; 9 | let hqSpawn = roomPlans(office)?.headquarters?.spawn.structure 10 | if (hqSpawn?.id !== spawn.id) return directions; 11 | const avoidPos = getTowerRefillerLocation(office); 12 | if (!avoidPos) return directions; 13 | const avoidDirection = hqSpawn?.pos.getDirectionTo(avoidPos) 14 | return directions.filter(d => d !== avoidDirection); 15 | } 16 | ) 17 | -------------------------------------------------------------------------------- /src/Selectors/energyInProduction.ts: -------------------------------------------------------------------------------- 1 | import { HarvestMission } from 'Missions/Implementations/HarvestMission'; 2 | import { LogisticsMission } from 'Missions/Implementations/LogisticsMission'; 3 | import { activeMissions, isMission } from 'Missions/Selectors'; 4 | import { memoizeByTick } from 'utils/memoizeFunction'; 5 | import { averageActiveFranchiseRoundTripDistance } from './Franchises/furthestActiveFranchiseRoundTripDistance'; 6 | 7 | export const energyInProduction = memoizeByTick( 8 | office => office, 9 | (office: string) => { 10 | // capacity per hauling cycle 11 | let harvestCapacity = 0; 12 | let haulCapacity = 0; 13 | // creep cost per hauling cycle 14 | let creepCost = 0; 15 | for (const mission of activeMissions(office)) { 16 | if (isMission(LogisticsMission)(mission)) { 17 | haulCapacity += mission.capacity(); 18 | creepCost += mission.creepCost(); 19 | } 20 | if (isMission(HarvestMission)(mission)) { 21 | harvestCapacity += mission.haulingCapacityNeeded(); 22 | creepCost += mission.creepCost(); 23 | } 24 | } 25 | 26 | const haulingCycles = CREEP_LIFE_TIME / averageActiveFranchiseRoundTripDistance(office) 27 | creepCost /= haulingCycles; 28 | 29 | return Math.max(0, Math.min(harvestCapacity, haulCapacity) - creepCost); 30 | } 31 | ); 32 | -------------------------------------------------------------------------------- /src/Selectors/energyInTransit.ts: -------------------------------------------------------------------------------- 1 | import { LogisticsMission } from 'Missions/Implementations/LogisticsMission'; 2 | import { activeMissions, isMission } from 'Missions/Selectors'; 3 | import { memoizeByTick } from 'utils/memoizeFunction'; 4 | import { franchiseEnergyAvailable } from './Franchises/franchiseEnergyAvailable'; 5 | import { franchisesByOffice } from './Franchises/franchisesByOffice'; 6 | 7 | export const energyInTransit = memoizeByTick( 8 | office => office, 9 | (office: string) => { 10 | // calculate fleet energy levels 11 | let fleetEnergy = 0; 12 | let fleetCapacity = 0; 13 | for (const mission of activeMissions(office).filter(isMission(LogisticsMission))) { 14 | fleetEnergy += mission.usedCapacity(); 15 | fleetCapacity += mission.capacity(); 16 | } 17 | 18 | // calculate franchise energy levels 19 | const franchiseEnergy = franchisesByOffice(office).reduce( 20 | (sum, { source }) => sum + franchiseEnergyAvailable(source), 21 | 0 22 | ); 23 | return fleetEnergy + Math.min(fleetCapacity, franchiseEnergy); 24 | } 25 | ); 26 | -------------------------------------------------------------------------------- /src/Selectors/findAlliedCreeps.ts: -------------------------------------------------------------------------------- 1 | import { WHITELIST } from 'config'; 2 | 3 | export const findAlliedCreeps = (room: string) => { 4 | if (!Game.rooms[room]) return []; 5 | 6 | // Return hostile creeps, if they are whitelisted 7 | return Game.rooms[room].find(FIND_HOSTILE_CREEPS, { filter: creep => WHITELIST.includes(creep.owner.username) }); 8 | }; 9 | 10 | export const findAlliedCreepsInRange = (pos: RoomPosition, range: number) => { 11 | if (!Game.rooms[pos.roomName]) return []; 12 | 13 | // Return hostile creeps, if they are whitelisted 14 | return pos.findInRange(FIND_HOSTILE_CREEPS, range, { filter: creep => WHITELIST.includes(creep.owner.username) }); 15 | }; 16 | 17 | export const findClosestAlliedCreepByRange = (pos: RoomPosition) => { 18 | if (!Game.rooms[pos.roomName]) return; 19 | return pos.findClosestByRange(FIND_HOSTILE_CREEPS, { filter: creep => WHITELIST.includes(creep.owner.username) }); 20 | }; 21 | -------------------------------------------------------------------------------- /src/Selectors/findHostileCreeps.ts: -------------------------------------------------------------------------------- 1 | import { WHITELIST } from "config"; 2 | import { memoizeByTick } from "utils/memoizeFunction"; 3 | 4 | export const findHostileCreeps = (room: string) => { 5 | if (!Game.rooms[room]) return []; 6 | 7 | // Return hostile creeps, if they aren't whitelisted 8 | return Game.rooms[room].find( 9 | FIND_HOSTILE_CREEPS, 10 | {filter: creep => !WHITELIST.includes(creep.owner.username)} 11 | ) 12 | } 13 | 14 | export const findHostileStructures = memoizeByTick( 15 | room => room, 16 | (room: string) => { 17 | if (!Game.rooms[room]) return []; 18 | 19 | // Return hostile creeps, if they aren't whitelisted 20 | return Game.rooms[room].find( 21 | FIND_HOSTILE_STRUCTURES, 22 | {filter: structure => structure.owner?.username && !WHITELIST.includes(structure.owner?.username)} 23 | ) 24 | } 25 | ); 26 | 27 | export const findInvaderStructures = (room: string) => { 28 | if (!Game.rooms[room]) return []; 29 | 30 | // Return hostile creeps, if they aren't whitelisted 31 | return Game.rooms[room].find( 32 | FIND_HOSTILE_STRUCTURES, 33 | {filter: structure => structure.structureType === STRUCTURE_INVADER_CORE} 34 | ) as StructureInvaderCore[] 35 | } 36 | 37 | export const findHostileCreepsInRange = (pos: RoomPosition, range: number) => { 38 | if (!Game.rooms[pos.roomName]) return []; 39 | 40 | // Return hostile creeps, if they aren't whitelisted 41 | return pos.findInRange( 42 | FIND_HOSTILE_CREEPS, 43 | range, 44 | {filter: creep => !WHITELIST.includes(creep.owner.username)} 45 | ) 46 | } 47 | 48 | export const findClosestHostileCreepByRange = (pos: RoomPosition) => { 49 | if (!Game.rooms[pos.roomName]) return; 50 | return pos.findClosestByRange( 51 | FIND_HOSTILE_CREEPS, 52 | {filter: creep => !WHITELIST.includes(creep.owner.username)} 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/Selectors/findReserveTargets.ts: -------------------------------------------------------------------------------- 1 | import { FranchiseObjectives } from "OldObjectives/Franchise"; 2 | import { posById } from "./posById"; 3 | 4 | export const findReserveTargets = (office: string) => { 5 | const franchises = new Set(); 6 | for (let o of Object.values(FranchiseObjectives)) { 7 | const room = posById(o.sourceId)?.roomName; 8 | if ( 9 | room && 10 | o.office === office && 11 | o.assigned.length >= 1 && 12 | !Memory.offices[room] && 13 | ( 14 | (Memory.rooms[room].reservation ?? 0) < 1000 || 15 | Memory.rooms[room].reserver !== 'LordGreywether' 16 | ) 17 | ) { 18 | franchises.add(room); 19 | } 20 | } 21 | return franchises; 22 | } 23 | -------------------------------------------------------------------------------- /src/Selectors/getActualEnergyAvailable.ts: -------------------------------------------------------------------------------- 1 | import { memoizeByTick } from "utils/memoizeFunction"; 2 | import { rcl } from "./rcl"; 3 | import { getEnergyStructures } from "./spawnsAndExtensionsDemand"; 4 | 5 | export const getActualEnergyAvailable = memoizeByTick( 6 | office => office, 7 | (office: string) => { 8 | if (Memory.rooms[office].rclMilestones?.[rcl(office) + 1]) { 9 | // Room is down-leveled, get capacity from active spawns/extensions 10 | return getEnergyStructures(office).reduce((sum, s) => sum + s.store.getUsedCapacity(RESOURCE_ENERGY), 0) 11 | } 12 | return Game.rooms[office]?.energyAvailable ?? 0 13 | } 14 | ) 15 | -------------------------------------------------------------------------------- /src/Selectors/getExitTiles.ts: -------------------------------------------------------------------------------- 1 | export const getExitTiles = (room: string) => { 2 | const terrain = Game.map.getRoomTerrain(room); 3 | const tiles: RoomPosition[] = []; 4 | 5 | for (let x = 0; x < 50; x += 1) { 6 | if (terrain.get(x, 0) !== TERRAIN_MASK_WALL) { 7 | tiles.push(new RoomPosition(x, 0, room)); 8 | } 9 | if (terrain.get(x, 49) !== TERRAIN_MASK_WALL) { 10 | tiles.push(new RoomPosition(x, 49, room)); 11 | } 12 | } 13 | for (let y = 1; y < 49; y += 1) { 14 | if (terrain.get(0, y) !== TERRAIN_MASK_WALL) { 15 | tiles.push(new RoomPosition(0, y, room)); 16 | } 17 | if (terrain.get(49, y) !== TERRAIN_MASK_WALL) { 18 | tiles.push(new RoomPosition(49, y, room)); 19 | } 20 | } 21 | 22 | return tiles; 23 | } 24 | -------------------------------------------------------------------------------- /src/Selectors/getExtensionsCapacity.ts: -------------------------------------------------------------------------------- 1 | import { rcl } from './rcl'; 2 | import { getExtensions } from './spawnsAndExtensionsDemand'; 3 | 4 | export const getExtensionsCapacity = (office: string) => { 5 | return ( 6 | getExtensions(office).reduce( 7 | (sum, e) => sum + ((e.structure as StructureExtension)?.store.getCapacity(RESOURCE_ENERGY) ?? 0), 8 | 0 9 | ) ?? 0 10 | ); 11 | }; 12 | export const approximateExtensionsCapacity = (office: string) => { 13 | // Instead of calculating the real capacity, estimate 14 | return CONTROLLER_STRUCTURES[STRUCTURE_EXTENSION][rcl(office)] * EXTENSION_ENERGY_CAPACITY[rcl(office)]; 15 | }; 16 | 17 | export const roomHasExtensions = (office: string) => { 18 | // If capacity is from more than spawns 19 | return getExtensions(office).some(e => e.structure); 20 | }; 21 | -------------------------------------------------------------------------------- /src/Selectors/getHqLocations.ts: -------------------------------------------------------------------------------- 1 | import { roomPlans } from './roomPlans'; 2 | 3 | const logisticsLocationCache = new Map(); 4 | export const getHeadquarterLogisticsLocation = (office: string) => { 5 | if (logisticsLocationCache.has(office)) return logisticsLocationCache.get(office); 6 | const plan = roomPlans(office)?.headquarters?.storage.pos; 7 | if (!plan) return; 8 | const pos = new RoomPosition(plan.x + 1, plan.y + 1, plan.roomName); 9 | if (!pos) return; 10 | logisticsLocationCache.set(office, pos); 11 | return pos; 12 | }; 13 | -------------------------------------------------------------------------------- /src/Selectors/getLabs.ts: -------------------------------------------------------------------------------- 1 | import { roomPlans } from "./roomPlans"; 2 | 3 | export const getLabs = (office: string) => { 4 | const labs = roomPlans(office)?.labs?.labs ?? []; 5 | const boostLabs = labs.filter(l => Memory.offices[office].lab.boostingLabs.some(b => b.id === l.structureId)); 6 | const reactionLabs = labs.filter(l => !boostLabs.includes(l)); 7 | return { 8 | inputs: reactionLabs.slice(0, 2), 9 | outputs: reactionLabs.slice(2), 10 | boosts: boostLabs, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Selectors/getOfficeDistance.ts: -------------------------------------------------------------------------------- 1 | import { getRoomPathDistance } from './Map/getRoomPathDistance'; 2 | import { getPath } from './Map/Pathing'; 3 | import { roomPlans } from './roomPlans'; 4 | 5 | export const getOfficeDistanceByRange = (office1: string, office2: string) => { 6 | return Game.map.getRoomLinearDistance(office1, office2); 7 | }; 8 | 9 | const pathCache = new Map(); 10 | export const getOfficeDistanceByPath = (office1: string, office2: string) => { 11 | const key = [office1, office2].sort().join(''); 12 | if (!pathCache.has(key)) { 13 | const pos1 = roomPlans(office1)?.headquarters?.storage.pos ?? new RoomPosition(25, 25, office1); 14 | const pos2 = roomPlans(office2)?.headquarters?.storage.pos ?? new RoomPosition(25, 25, office2); 15 | const path = getPath(pos1, pos2, 1); 16 | if (path) pathCache.set(key, path); 17 | } 18 | return pathCache.get(key)?.cost; 19 | }; 20 | 21 | const roomPathCache = new Map(); 22 | export const getOfficeDistanceByRoomPath = (office1: string, office2: string) => { 23 | const key = [office1, office2].sort().join(''); 24 | if (!roomPathCache.has(key)) { 25 | const distance = getRoomPathDistance(office1, office2); 26 | if (distance) roomPathCache.set(key, distance); 27 | } 28 | return roomPathCache.get(key); 29 | }; 30 | -------------------------------------------------------------------------------- /src/Selectors/getPatrolRoute.ts: -------------------------------------------------------------------------------- 1 | import profiler from "utils/profiler"; 2 | import { calculateNearbyRooms } from "./Map/MapCoordinates"; 3 | 4 | let patrols = new Map(); 5 | 6 | export const getPatrolRoute = profiler.registerFN((office: string) => { 7 | if (!patrols.has(office)) { 8 | patrols.set(office, generatePatrolRoute(office)); 9 | } 10 | return patrols.get(office) as string[]; 11 | }, 'getPatrolRoute') 12 | 13 | /** 14 | * Generates a naive patrol route sorted by proximity to the central room 15 | */ 16 | const generatePatrolRoute = (office: string) => { 17 | // console.log('generating patrol route for', office) 18 | let surveyRadius = (Game.rooms[office]?.controller?.level !== 8) ? 5 : 20 19 | let rooms = calculateNearbyRooms(office, surveyRadius, false).sort((a, b) => 20 | Game.map.getRoomLinearDistance(a, office) - Game.map.getRoomLinearDistance(b, office) 21 | ) 22 | return rooms; 23 | } 24 | -------------------------------------------------------------------------------- /src/Selectors/getPrimarySpawn.ts: -------------------------------------------------------------------------------- 1 | import { getSpawns } from "./roomPlans"; 2 | 3 | export const getPrimarySpawn = (roomName: string) => { 4 | return getSpawns(roomName)[0] 5 | } 6 | -------------------------------------------------------------------------------- /src/Selectors/getScientists.ts: -------------------------------------------------------------------------------- 1 | const cache = new Map(); 2 | 3 | export const registerScientists = (office: string, creeps: Creep[]) => { 4 | cache.set( 5 | office, 6 | creeps.map(c => c.name) 7 | ); 8 | }; 9 | export const getScientists = (office: string) => { 10 | const creeps = (cache.get(office) ?? []).map(name => Game.creeps[name]).filter(c => !!c); 11 | registerScientists(office, creeps); 12 | return creeps; 13 | }; 14 | -------------------------------------------------------------------------------- /src/Selectors/getSpawnCost.ts: -------------------------------------------------------------------------------- 1 | import { getSpawns } from "./roomPlans"; 2 | 3 | const spawning = new Set() 4 | 5 | export function getSpawnCost(office: string) { 6 | let cost = 0; 7 | for (let c of spawning) { 8 | if (!Game.creeps[c]?.spawning) spawning.delete(c); 9 | } 10 | getSpawns(office).forEach(s => { 11 | if (s.spawning && !spawning.has(s.spawning.name) && Game.creeps[s.spawning.name]) { 12 | spawning.add(s.spawning.name); 13 | cost += Game.creeps[s.spawning.name].body.reduce((sum, p) => sum + BODYPART_COST[p.type], 0) 14 | } 15 | }) 16 | return cost; 17 | } 18 | -------------------------------------------------------------------------------- /src/Selectors/hasEnergyIncome.ts: -------------------------------------------------------------------------------- 1 | import { HarvestMission } from 'Missions/Implementations/HarvestMission'; 2 | import { LogisticsMission } from 'Missions/Implementations/LogisticsMission'; 3 | import { activeMissions, isMission } from 'Missions/Selectors'; 4 | import { memoizeByTick } from 'utils/memoizeFunction'; 5 | import { storageEnergyAvailable } from './storageEnergyAvailable'; 6 | 7 | export const hasEnergyIncome = memoizeByTick( 8 | office => office, 9 | (office: string): boolean => { 10 | const harvestMissions = activeMissions(office) 11 | .filter(isMission(HarvestMission)) 12 | .some(m => m.harvestRate() > 0); 13 | const logisticsMissions = activeMissions(office) 14 | .filter(isMission(LogisticsMission)) 15 | .some(m => m.capacity() > 0); 16 | return ( 17 | (harvestMissions && logisticsMissions) || 18 | storageEnergyAvailable(office) > Game.rooms[office].energyCapacityAvailable 19 | ); 20 | } 21 | ); 22 | -------------------------------------------------------------------------------- /src/Selectors/ingredientForLabsObjective.ts: -------------------------------------------------------------------------------- 1 | 2 | export function ingredientForLabsObjective(office: string) { 3 | return [RESOURCE_KEANIUM, RESOURCE_HYDROGEN] 4 | } 5 | -------------------------------------------------------------------------------- /src/Selectors/ingredientsNeededForLabOrder.ts: -------------------------------------------------------------------------------- 1 | import { LabOrder } from 'Structures/Labs/LabOrder'; 2 | import { getLabs } from './getLabs'; 3 | 4 | export function ingredientsNeededForLabOrder(office: string, order: LabOrder, scientists: Creep[]) { 5 | // In Process ingredients are in Scientists' inventories or input labs 6 | // In Process products are in Scientists' inventories or output labs 7 | const { inputs, outputs } = getLabs(office); 8 | 9 | const product = 10 | scientists.reduce((sum, c) => sum + c.store.getUsedCapacity(order.output), 0) + 11 | outputs.reduce((sum, c) => sum + ((c.structure as StructureLab)?.store.getUsedCapacity(order.output) ?? 0), 0); 12 | 13 | const ingredient1 = 14 | scientists.reduce((sum, c) => sum + c.store.getUsedCapacity(order.ingredient1), 0) + 15 | inputs.reduce((sum, c) => sum + ((c.structure as StructureLab)?.store.getUsedCapacity(order.ingredient1) ?? 0), 0); 16 | 17 | const ingredient2 = 18 | scientists.reduce((sum, c) => sum + c.store.getUsedCapacity(order.ingredient2), 0) + 19 | inputs.reduce((sum, c) => sum + ((c.structure as StructureLab)?.store.getUsedCapacity(order.ingredient2) ?? 0), 0); 20 | 21 | const target = order.amount - product; 22 | 23 | const roundToNextHighest = (increment: number, value: number) => Math.ceil(value / increment) * increment; 24 | 25 | return { 26 | ingredient1: Math.max(0, roundToNextHighest(5, target - ingredient1)), 27 | ingredient2: Math.max(0, roundToNextHighest(5, target - ingredient2)) 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/Selectors/isSpawned.ts: -------------------------------------------------------------------------------- 1 | export const isSpawned = (creep?: Creep) => creep && !creep.spawning; 2 | -------------------------------------------------------------------------------- /src/Selectors/labsShouldBeEmptied.ts: -------------------------------------------------------------------------------- 1 | import { LabOrder } from "Structures/Labs/LabOrder"; 2 | import { getLabs } from "./getLabs"; 3 | 4 | export function labsShouldBeEmptied(office: string) { 5 | const order = Memory.offices[office].lab.orders?.find(o => o.amount > 0) as LabOrder|undefined; 6 | const { inputs, outputs } = getLabs(office); 7 | const [lab1, lab2] = inputs.map(s => s.structure) as (StructureLab|undefined)[]; 8 | 9 | // if no order, and there are resources in labs, then labs should be emptied 10 | if (!order && [...inputs, ...outputs].some(l => (l.structure as StructureLab|undefined)?.mineralType)) { 11 | return true; 12 | } else if (!order) { 13 | return false 14 | } 15 | 16 | // Return true if input labs have foreign ingredients, or output labs have an old product 17 | return ( 18 | (lab1?.mineralType && lab1?.mineralType !== order.ingredient1) || 19 | (lab2?.mineralType && lab2?.mineralType !== order.ingredient2) || 20 | outputs.some(l => (l.structure as StructureLab|undefined)?.mineralType && (l.structure as StructureLab|undefined)?.mineralType !== order.output) 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/Selectors/linkUsedCapacity.ts: -------------------------------------------------------------------------------- 1 | export const linkUsedCapacity = (link: StructureLink|undefined) => { 2 | if (!link) return 0; 3 | return link.store.getUsedCapacity(RESOURCE_ENERGY) / link.store.getCapacity(RESOURCE_ENERGY) 4 | } 5 | -------------------------------------------------------------------------------- /src/Selectors/logCostMatrix.ts: -------------------------------------------------------------------------------- 1 | export const logCostMatrix = (cm: CostMatrix) => { 2 | for (let y = 0; y < 49; y++) { 3 | let line = ''; 4 | for (let x = 0; x < 49; x++) { 5 | line += (cm.get(x, y) + ' ').slice(0, 3); 6 | } 7 | console.log(line); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Selectors/marketEnabled.ts: -------------------------------------------------------------------------------- 1 | import { memoizeByTick } from "utils/memoizeFunction"; 2 | 3 | let enabled: boolean|undefined = undefined; 4 | export const marketEnabled = () => { 5 | if (enabled === undefined) enabled = !!allMarketOrders().length; 6 | return enabled; 7 | } 8 | 9 | export const allMarketOrders = memoizeByTick( 10 | () => '', 11 | () => Game.market.getAllOrders() 12 | ) 13 | -------------------------------------------------------------------------------- /src/Selectors/minionCostPerTick.ts: -------------------------------------------------------------------------------- 1 | import { CreepBuild } from 'Minions/Builds/utils'; 2 | import { boostCost } from './Market/boostCost'; 3 | 4 | export const minionCost = (body: BodyPartConstant[]) => { 5 | return body.reduce((sum, p) => sum + BODYPART_COST[p], 0); 6 | }; 7 | 8 | export const buildCost = (body: CreepBuild['body'], boosts: CreepBuild['boosts']) => { 9 | return minionCost(body) + boosts.reduce((sum, { type, count }) => sum + count * boostCost(type), 0); // TODO: Add boost costs 10 | }; 11 | 12 | export const maxBuildCost = (builds: CreepBuild[]) => { 13 | return Math.max(...builds.map(b => buildCost(b.body, b.boosts)), 0); 14 | }; 15 | 16 | export const minionCostPerTick = (body: BodyPartConstant[]) => { 17 | const lifetime = body.includes(CLAIM) ? CREEP_CLAIM_LIFE_TIME : CREEP_LIFE_TIME; 18 | return minionCost(body) / lifetime; 19 | }; 20 | 21 | export const creepCost = (creep: Creep) => { 22 | return minionCost(creep.body.map(p => p.type)); 23 | }; 24 | 25 | export const creepCostPerTick = (creep: Creep) => { 26 | return minionCostPerTick(creep.body.map(p => p.type)); 27 | }; 28 | -------------------------------------------------------------------------------- /src/Selectors/missionCpuAvailable.ts: -------------------------------------------------------------------------------- 1 | import { CPU_ESTIMATE_PERIOD } from 'config'; 2 | import { cpuOverhead } from './cpuOverhead'; 3 | 4 | export const missionCpuAvailable = (office: string) => { 5 | const baseCpu = Math.max(0, (Game.cpu.bucket - 500 + (Game.cpu.limit - cpuOverhead()) * CPU_ESTIMATE_PERIOD) * 0.5); 6 | const offices = Object.keys(Memory.offices).length; 7 | return baseCpu / offices; 8 | }; 9 | -------------------------------------------------------------------------------- /src/Selectors/missionEnergyAvailable.ts: -------------------------------------------------------------------------------- 1 | import { energyInProduction } from './energyInProduction'; 2 | import { roomEnergyAvailable } from './storageEnergyAvailable'; 3 | 4 | export const missionEnergyAvailable = (office: string) => { 5 | // Energy in storage/production - room energy deficit = total energy available for budgeting 6 | // energyInProduction(office, estimateMissionInterval(office)) 7 | let energy = 8 | Math.max(roomEnergyAvailable(office), energyInProduction(office)) - 9 | (Game.rooms[office].energyCapacityAvailable - Game.rooms[office].energyAvailable); 10 | return energy; 11 | }; 12 | -------------------------------------------------------------------------------- /src/Selectors/officeIsDownleveled.ts: -------------------------------------------------------------------------------- 1 | import { rcl } from "./rcl" 2 | 3 | export const officeIsDownleveled = (office: string) => { 4 | return rcl(office) < Math.max(...Object.keys(Memory.rooms[office].rclMilestones ?? {}).map(Number)) 5 | } 6 | -------------------------------------------------------------------------------- /src/Selectors/officeResourceSurplus.ts: -------------------------------------------------------------------------------- 1 | import { roomPlans } from "./roomPlans"; 2 | 3 | export function officeResourceSurplus(office: string) { 4 | const totals = new Map(); 5 | const plan = roomPlans(office)?.headquarters; 6 | for (let [resource, amount] of Object.entries(Memory.offices[office].resourceQuotas)) { 7 | if (resource === undefined || amount === undefined) continue; 8 | totals.set( 9 | resource as ResourceConstant, 10 | -amount + ((plan?.terminal.structure as StructureTerminal)?.store.getUsedCapacity(resource as ResourceConstant) ?? 0) 11 | ); 12 | } 13 | return totals; 14 | } 15 | -------------------------------------------------------------------------------- /src/Selectors/officeShouldMine.ts: -------------------------------------------------------------------------------- 1 | import { byId } from "./byId" 2 | import { mineralId } from "./roomCache" 3 | 4 | export const officeShouldMine = (office: string) => { 5 | const mineral = byId(mineralId(office))?.mineralType 6 | const terminal = Game.rooms[office]?.terminal 7 | 8 | if (!mineral || !terminal) return false; // No mineral or no terminal 9 | 10 | const target = Memory.offices[office].resourceQuotas[mineral] ?? 2000 11 | const actual = terminal.store.getUsedCapacity(mineral) 12 | if (actual >= target * 2) return false; 13 | 14 | return true; 15 | } 16 | -------------------------------------------------------------------------------- /src/Selectors/ownedMinerals.ts: -------------------------------------------------------------------------------- 1 | import { byId } from "./byId"; 2 | import { mineralId } from "./roomCache"; 3 | 4 | export const ownedMinerals = () => { 5 | const minerals = new Set(); 6 | for (let office in Memory.offices) { 7 | const mineral = byId(mineralId(office))?.mineralType 8 | if (mineral) minerals.add(mineral) 9 | } 10 | return minerals; 11 | } 12 | -------------------------------------------------------------------------------- /src/Selectors/plannedActiveFranchiseRoads.ts: -------------------------------------------------------------------------------- 1 | import { getCachedPath } from 'screeps-cartographer'; 2 | import { franchiseIsThreatened } from 'Strategy/Territories/HarassmentZones'; 3 | import { plannedFranchiseRoads } from './plannedFranchiseRoads'; 4 | import { sourceIds } from './roomCache'; 5 | 6 | export function plannedActiveFranchiseRoads(office: string) { 7 | return [ 8 | ...new Set( 9 | (Memory.offices[office]?.territories ?? []) 10 | .flatMap(t => sourceIds(t)) 11 | .filter( 12 | source => 13 | (Memory.offices[office].franchises[source]?.lastActive ?? 0) + 2000 > Game.time && 14 | !franchiseIsThreatened(office, source) 15 | ) 16 | .sort((a, b) => (getCachedPath(office + a)?.length ?? 0) - (getCachedPath(office + b)?.length ?? 0)) 17 | .flatMap(source => { 18 | return plannedFranchiseRoads(office, source); 19 | }) 20 | ) 21 | ]; 22 | } 23 | -------------------------------------------------------------------------------- /src/Selectors/plannedStructures.ts: -------------------------------------------------------------------------------- 1 | import { PlannedStructure } from "RoomPlanner/PlannedStructure"; 2 | 3 | export const serializePlannedStructures = (structures: PlannedStructure[]) => { 4 | let serializedStructures = ''; 5 | for (let s of structures) { 6 | serializedStructures += s.serialize(); 7 | } 8 | return serializedStructures; 9 | } 10 | export const deserializePlannedStructures = (serializedStructures: string) => { 11 | let structures: PlannedStructure[] = []; 12 | if (serializedStructures.length < 3) return structures; 13 | for (let i = 0; i < serializedStructures.length; i += 3) { 14 | structures.push(PlannedStructure.deserialize(serializedStructures.slice(i, i+3))) 15 | } 16 | return structures; 17 | } 18 | -------------------------------------------------------------------------------- /src/Selectors/posById.ts: -------------------------------------------------------------------------------- 1 | import { unpackPos } from "utils/packrat"; 2 | 3 | export const posById = (id?: Id) => { 4 | if (!id) return undefined; 5 | const pos = Memory.positions[id]; 6 | if (!pos) return undefined; 7 | return unpackPos(pos); 8 | } 9 | -------------------------------------------------------------------------------- /src/Selectors/prespawn.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if creep doesn't need to be replaced 3 | * Can filter a list of creeps to those that don't need to be replaced 4 | */ 5 | export const prespawnByArrived = (creep: Creep) => 6 | creep.ticksToLive === undefined || creep.memory.arrived === undefined || creep.ticksToLive > creep.memory.arrived; 7 | 8 | /** 9 | * Set arrived timestamp, if not already set 10 | */ 11 | export const setArrived = (creep: Creep) => { 12 | const lifetime = creep.body.some(p => p.type === CLAIM) ? CREEP_CLAIM_LIFE_TIME : CREEP_LIFE_TIME; 13 | if (!creep.memory.arrived && creep.ticksToLive) { 14 | creep.memory.arrived = lifetime - creep.ticksToLive; // creep life time 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/Selectors/rcl.ts: -------------------------------------------------------------------------------- 1 | export const rcl = (room: string) => { 2 | return Game.rooms[room]?.controller?.level ?? 0 3 | } 4 | -------------------------------------------------------------------------------- /src/Selectors/reducers.ts: -------------------------------------------------------------------------------- 1 | export const sum = (a: number, b: number) => a + b; 2 | export const min = 3 | (value: (v: T) => number) => 4 | (a?: T, b?: T) => { 5 | if (!a && b) return b; 6 | if (!b && a) return a; 7 | if (!a || !b) return undefined; 8 | if (value(b) < value(a)) return b; 9 | return a; 10 | }; 11 | -------------------------------------------------------------------------------- /src/Selectors/renewCost.ts: -------------------------------------------------------------------------------- 1 | import { minionCost } from "./minionCostPerTick"; 2 | 3 | export const renewCost = (creep: Creep) => (minionCost(creep.body.map(p => p.type)) / 2.5 / creep.body.length); 4 | -------------------------------------------------------------------------------- /src/Selectors/repairCostsPerTick.ts: -------------------------------------------------------------------------------- 1 | import { memoize } from "utils/memoizeFunction"; 2 | import { plannedStructuresByRcl } from "./plannedStructuresByRcl"; 3 | import { rcl } from "./rcl"; 4 | 5 | export const repairCostsPerTick = memoize( 6 | office => office + rcl(office), 7 | (office: string) => { 8 | const structures = plannedStructuresByRcl(office, rcl(office)) 9 | let decayPerTick = 0 10 | for (let s of structures) { 11 | if (s.structureType === STRUCTURE_ROAD) { 12 | decayPerTick += ROAD_DECAY_AMOUNT / ROAD_DECAY_TIME; 13 | } else if (s.structureType === STRUCTURE_CONTAINER) { 14 | decayPerTick += CONTAINER_DECAY / (Game.rooms[s.pos.roomName]?.controller?.my ? CONTAINER_DECAY_TIME_OWNED : CONTAINER_DECAY_TIME) 15 | } else if (s.structureType === STRUCTURE_RAMPART) { 16 | decayPerTick += RAMPART_DECAY_AMOUNT / RAMPART_DECAY_TIME 17 | } 18 | } 19 | return decayPerTick / REPAIR_POWER; 20 | } 21 | ) 22 | -------------------------------------------------------------------------------- /src/Selectors/reservations.ts: -------------------------------------------------------------------------------- 1 | export function isReservedByEnemy(room: string) { 2 | return Memory.rooms[room].reserver && Memory.rooms[room].reserver !== 'LordGreywether'; 3 | } 4 | 5 | export function isOwnedByEnemy(room: string) { 6 | return Memory.rooms[room].owner && Memory.rooms[room].owner !== 'LordGreywether'; 7 | } 8 | -------------------------------------------------------------------------------- /src/Selectors/reserveCapacity.ts: -------------------------------------------------------------------------------- 1 | export const getReservedUsedCapacity = (target: AnyStoreStructure | Creep, resource = RESOURCE_ENERGY) => { 2 | return target.store[resource]; 3 | }; 4 | export const getReservedFreeCapacity = (target: AnyStoreStructure | Creep, resource = RESOURCE_ENERGY) => { 5 | return target.store.getCapacity(resource) - target.store[resource]; 6 | }; 7 | export const reserveCapacity = (target: AnyStoreStructure | Creep, amount: number, resource = RESOURCE_ENERGY) => { 8 | target.store[resource] = Math.min(getReservedFreeCapacity(target), amount); 9 | }; 10 | -------------------------------------------------------------------------------- /src/Selectors/resourcesNearPos.ts: -------------------------------------------------------------------------------- 1 | export const resourcesNearPos = (pos: RoomPosition, radius = 1, resource?: ResourceConstant) => { 2 | // const results = Game.rooms[pos.roomName]?.lookForAtArea( 3 | // LOOK_RESOURCES, 4 | // pos.y - radius, 5 | // pos.x - radius, 6 | // pos.y + radius, 7 | // pos.x + radius, 8 | // true 9 | // ) 10 | // return results?.map(r => r.resource).filter(r => !resource || r.resourceType === resource).sort((a, b) => b.amount - a.amount) ?? [] 11 | 12 | return pos.findInRange(FIND_DROPPED_RESOURCES, radius, { filter: r => !resource || r.resourceType === resource }).sort((a, b) => b.amount - a.amount) 13 | } 14 | -------------------------------------------------------------------------------- /src/Selectors/roomCache.ts: -------------------------------------------------------------------------------- 1 | import { memoize } from 'utils/memoizeFunction'; 2 | import { posById } from './posById'; 3 | 4 | export const sourceIds = (roomName: string) => 5 | Memory.rooms[roomName]?.sourceIds?.filter(s => s) ?? ([] as Id[]); 6 | export const sourcePositions = (roomName: string) => 7 | sourceIds(roomName) 8 | .map(id => posById(id)) 9 | .filter(s => s) as RoomPosition[]; 10 | 11 | export const mineralId = (roomName: string) => Memory.rooms[roomName]?.mineralId as Id | undefined; 12 | export const mineralPosition = (roomName: string) => posById(mineralId(roomName)); 13 | 14 | export const controllerId = (roomName: string) => 15 | Memory.rooms[roomName]?.controllerId as Id | undefined; 16 | export const controllerPosition = (roomName: string) => posById(controllerId(roomName)); 17 | 18 | export const roomExits = memoize( 19 | roomName => roomName, 20 | (roomName: string) => { 21 | const exits = []; 22 | for (let x = 0; x < 50; x += 1) { 23 | exits.push(new RoomPosition(x, 0, roomName)); 24 | exits.push(new RoomPosition(x, 49, roomName)); 25 | } 26 | for (let y = 1; y < 49; y += 1) { 27 | exits.push(new RoomPosition(0, y, roomName)); 28 | exits.push(new RoomPosition(49, y, roomName)); 29 | } 30 | const terrain = Game.map.getRoomTerrain(roomName); 31 | return exits.filter(pos => terrain.get(pos.x, pos.y) !== TERRAIN_MASK_WALL); // any border squares that aren't walls must be exits 32 | } 33 | ); 34 | -------------------------------------------------------------------------------- /src/Selectors/roomIsEligibleForOffice.ts: -------------------------------------------------------------------------------- 1 | import { countTerrainTypes } from "./Map/MapCoordinates"; 2 | 3 | /** 4 | * Requires vision 5 | */ 6 | export const roomIsEligibleForOffice = (roomName: string) => { 7 | // Room must have a controller and two sources 8 | // To avoid edge cases, controller and sources must not be within range 5 of each other or an exit square 9 | let controller = Game.rooms[roomName].controller?.pos 10 | if (!controller) { 11 | console.log(`Room planning for ${roomName} failed - No controller`); 12 | return false; 13 | } 14 | let sources = Game.rooms[roomName].find(FIND_SOURCES).map(s => s.pos); 15 | if (!sources || sources.length < 2) { 16 | console.log(`Room planning for ${roomName} failed - Invalid number of sources`); 17 | return false; 18 | } 19 | 20 | let [source1, source2] = sources; 21 | if (controller.findClosestByRange(FIND_EXIT)?.inRangeTo(controller, 2)) { 22 | console.log(`Room planning for ${roomName} failed - Controller too close to exit`); 23 | return false; 24 | } 25 | if (sources.some(s => s.findClosestByRange(FIND_EXIT)?.inRangeTo(s, 2))) { 26 | console.log(`Room planning for ${roomName} failed - Source too close to exit`); 27 | return false; 28 | } 29 | if (source1.getRangeTo(source2) < 3) { 30 | console.log(`Room planning for ${roomName} failed - Sources too close together`); 31 | return false; 32 | } 33 | 34 | const terrainTypeCount = countTerrainTypes(roomName); 35 | 36 | if ((terrainTypeCount.swamp * 1.5) > terrainTypeCount.plains) { 37 | console.log(`Room planning for ${roomName} failed - Too much swamp`); 38 | return false; 39 | } 40 | return true; 41 | } 42 | -------------------------------------------------------------------------------- /src/Selectors/scheduledCallbacks.ts: -------------------------------------------------------------------------------- 1 | const queue = new Map void)[]>(); 2 | 3 | export const schedule = (fn: () => void, ticks: number) => { 4 | const list = queue.get(Game.time + ticks) ?? []; 5 | list.push(fn); 6 | queue.set(Game.time + ticks, list); 7 | }; 8 | 9 | export const runScheduled = () => { 10 | for (const [k, entries] of queue) { 11 | if (k <= Game.time) { 12 | entries.forEach(fn => fn()); 13 | queue.delete(k); 14 | } 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/Selectors/spawnEnergyAvailable.ts: -------------------------------------------------------------------------------- 1 | import { heapMetrics } from "Metrics/heapMetrics"; 2 | import { Metrics } from "screeps-viz"; 3 | import { memoizeByTick } from "utils/memoizeFunction"; 4 | 5 | export const spawnEnergyAvailable = memoizeByTick( 6 | (room: string) => room, 7 | (room: string) => { 8 | return Math.max( 9 | 300, 10 | heapMetrics[room]?.roomEnergy.values.length ? Metrics.max(heapMetrics[room].roomEnergy)[1] : (Game.rooms[room]?.energyCapacityAvailable ?? 0) 11 | ); 12 | }) 13 | -------------------------------------------------------------------------------- /src/Selectors/spawnsAndExtensionsDemand.ts: -------------------------------------------------------------------------------- 1 | import { PlannedStructure } from 'RoomPlanner/PlannedStructure'; 2 | import { memoizeByTick } from 'utils/memoizeFunction'; 3 | import { rcl } from './rcl'; 4 | import { getSpawns, roomPlans } from './roomPlans'; 5 | 6 | export const getExtensions = (room: string, includeFastfiller = true) => { 7 | const plan = roomPlans(room); 8 | if (!plan) return []; 9 | return ([] as (PlannedStructure | undefined)[]) 10 | .concat( 11 | includeFastfiller ? plan.fastfiller?.extensions ?? [] : [], 12 | plan.franchise1?.extensions ?? [], 13 | plan.franchise2?.extensions ?? [], 14 | plan.headquarters?.extension, 15 | plan.extensions?.extensions ?? [], 16 | plan.backfill?.extensions ?? [] 17 | ) 18 | .filter((s): s is PlannedStructure => !!s); 19 | }; 20 | 21 | export const getEnergyStructures = memoizeByTick( 22 | room => room, 23 | (room: string) => { 24 | const plan = roomPlans(room); 25 | if (!plan) return []; 26 | const structures = getExtensions(room) 27 | .map(s => s?.structure) 28 | .concat(getSpawns(room)) 29 | .filter(s => s) as (StructureExtension | StructureSpawn)[]; 30 | 31 | if (Memory.rooms[room].rclMilestones?.[rcl(room) + 1]) { 32 | // Room is downleveled 33 | return structures.filter(e => e.isActive()); 34 | } 35 | return structures; 36 | } 37 | ); 38 | 39 | export const extensionsDemand = (room: string) => { 40 | return getExtensions(room).reduce((sum, s) => { 41 | return sum + ((s.structure as StructureExtension)?.store.getFreeCapacity(RESOURCE_ENERGY) ?? 0); 42 | }, 0); 43 | }; 44 | -------------------------------------------------------------------------------- /src/Selectors/storageEnergyAvailable.ts: -------------------------------------------------------------------------------- 1 | import { memoizeByTick } from 'utils/memoizeFunction'; 2 | import { getPrimarySpawn } from './getPrimarySpawn'; 3 | import { roomPlans } from './roomPlans'; 4 | 5 | export const storageEnergyAvailable = (roomName: string) => { 6 | const plan = roomPlans(roomName); 7 | if (!plan?.headquarters && !plan?.fastfiller) return 0; 8 | if (!plan.fastfiller?.containers.some(c => c.structure) && !plan.headquarters?.storage.structure) 9 | return getPrimarySpawn(roomName)?.store.getUsedCapacity(RESOURCE_ENERGY) ?? 0; 10 | return ( 11 | (plan.headquarters?.storage.structure?.store.getUsedCapacity(RESOURCE_ENERGY) ?? 0) + 12 | (plan.fastfiller?.containers.reduce( 13 | (sum, c) => sum + (c.structure?.store.getUsedCapacity(RESOURCE_ENERGY) ?? 0), 14 | 0 15 | ) ?? 0) 16 | ); 17 | }; 18 | 19 | export const fastfillerIsFull = (roomName: string) => { 20 | const plan = roomPlans(roomName)?.fastfiller; 21 | if (!plan) return true; 22 | return ( 23 | plan.containers.every(c => !c.structure || c.structure.store.getFreeCapacity(RESOURCE_ENERGY) === 0) && 24 | plan.extensions.every(c => !c.structure || c.structure.store.getFreeCapacity(RESOURCE_ENERGY) === 0) && 25 | plan.spawns.every(c => !c.structure || c.structure.store.getFreeCapacity(RESOURCE_ENERGY) === 0) 26 | ); 27 | }; 28 | 29 | export const roomEnergyAvailable = memoizeByTick( 30 | office => office, 31 | (office: string) => { 32 | const plan = roomPlans(office); 33 | return ( 34 | (plan?.headquarters?.storage.structure?.store.getUsedCapacity(RESOURCE_ENERGY) ?? 0) + 35 | (plan?.library?.container.structure?.store.getUsedCapacity(RESOURCE_ENERGY) ?? 0) + 36 | (plan?.fastfiller?.containers.reduce( 37 | (sum, c) => sum + (c.structure?.store.getUsedCapacity(RESOURCE_ENERGY) ?? 0), 38 | 0 39 | ) ?? 0) 40 | ); 41 | } 42 | ); 43 | -------------------------------------------------------------------------------- /src/Selectors/terminalBalance.ts: -------------------------------------------------------------------------------- 1 | import { roomPlans } from "./roomPlans"; 2 | 3 | export function terminalBalance(office: string, resource: ResourceConstant) { 4 | return (roomPlans(office)?.headquarters?.terminal.structure as StructureTerminal|undefined)?.store.getUsedCapacity(resource) ?? 0 5 | } 6 | -------------------------------------------------------------------------------- /src/Selectors/territoryIntent.ts: -------------------------------------------------------------------------------- 1 | import { controllerId, sourceIds } from './roomCache'; 2 | 3 | export enum TerritoryIntent { 4 | AVOID = 'AVOID', 5 | ACQUIRE = 'ACQUIRE', 6 | DEFEND = 'DEFEND', 7 | EXPLOIT = 'EXPLOIT', 8 | IGNORE = 'IGNORE', 9 | PLUNDER = 'PLUNDER' 10 | } 11 | 12 | export const getTerritoryIntent = (roomName: string) => { 13 | let controller = controllerId(roomName); 14 | let sources = sourceIds(roomName); 15 | const hostiles = Game.time - (Memory.rooms[roomName]?.lastHostileSeen ?? 0) <= 10; 16 | 17 | if (!controller) { 18 | return TerritoryIntent.IGNORE; 19 | } 20 | 21 | if (Memory.rooms[roomName]?.plunder?.resources.length) { 22 | return TerritoryIntent.PLUNDER; 23 | } 24 | 25 | if ( 26 | Memory.rooms[roomName]?.owner && 27 | !Game.rooms[roomName]?.controller?.my 28 | // (Memory.rooms[roomName]?.reserver && Memory.rooms[roomName]?.reserver !== 'LordGreywether' && Memory.rooms[roomName]?.reserver !== 'Invader') 29 | ) { 30 | return TerritoryIntent.AVOID; 31 | } else if (hostiles && Memory.offices[roomName]) { 32 | // Owned Office has hostiles present, recently 33 | return TerritoryIntent.DEFEND; 34 | } else if (sources.length > 0) { 35 | return TerritoryIntent.EXPLOIT; 36 | } else { 37 | return TerritoryIntent.IGNORE; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/Selectors/towerDamage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on https://github.com/screeps/engine/blob/master/src/processor/intents/towers/attack.js#L32 3 | */ 4 | export const towerDamage = (tower?: StructureTower, pos?: RoomPosition) => { 5 | if (!tower || !pos) return 0; 6 | const range = Math.min(TOWER_FALLOFF_RANGE, tower.pos.getRangeTo(pos)); 7 | let amount = TOWER_POWER_ATTACK; 8 | if(range > TOWER_OPTIMAL_RANGE) { 9 | amount -= amount * TOWER_FALLOFF * (range - TOWER_OPTIMAL_RANGE) / (TOWER_FALLOFF_RANGE - TOWER_OPTIMAL_RANGE); 10 | } 11 | return amount; 12 | } 13 | -------------------------------------------------------------------------------- /src/Selectors/typeguards.ts: -------------------------------------------------------------------------------- 1 | import { PlannedStructure } from 'RoomPlanner/PlannedStructure'; 2 | 3 | export const isUndefined = (item: any): item is undefined => item === undefined; 4 | 5 | export const isRoomPosition = (item: any): item is RoomPosition => item instanceof RoomPosition; 6 | 7 | export const isCreep = (item: any): item is Creep => item instanceof Creep; 8 | 9 | export const isPlannedStructure = 10 | (type?: T) => 11 | (structure?: PlannedStructure): structure is PlannedStructure => 12 | !!structure && (!type || structure.structureType === type); 13 | 14 | export const isNotNull = (b: T): b is Exclude => b !== null; 15 | -------------------------------------------------------------------------------- /src/Selectors/validatePathsToPointsOfInterest.ts: -------------------------------------------------------------------------------- 1 | import { getExitTiles } from "./getExitTiles"; 2 | import { controllerPosition, mineralPosition, sourcePositions } from "./roomCache"; 3 | import { isRoomPosition } from "./typeguards"; 4 | 5 | /** 6 | * Returns true if there is a path from origin to 7 | * each source, the mineral, the controller, and at 8 | * least one exit square 9 | */ 10 | export const validatePathsToPointsOfInterest = (room: string, costMatrix: CostMatrix, origin: RoomPosition) => { 11 | const pointsOfInterest = ([] as (RoomPosition|undefined)[]).concat( 12 | sourcePositions(room), 13 | [ 14 | mineralPosition(room), 15 | controllerPosition(room) 16 | ] 17 | ).filter(isRoomPosition) 18 | const exits = getExitTiles(room); 19 | 20 | for (const pos of pointsOfInterest) { 21 | const path = PathFinder.search( 22 | origin, 23 | {pos, range: 1}, 24 | {maxRooms: 1, roomCallback: () => costMatrix, plainCost: 2, swampCost: 10} 25 | ); 26 | if (path.incomplete) return false; 27 | } 28 | for (const pos of exits) { 29 | const path = PathFinder.search( 30 | origin, 31 | {pos, range: 1}, 32 | {maxRooms: 1, roomCallback: () => costMatrix, plainCost: 2, swampCost: 10} 33 | ); 34 | if (!path.incomplete) return true; 35 | } 36 | return false; 37 | } 38 | -------------------------------------------------------------------------------- /src/Selectors/viz.ts: -------------------------------------------------------------------------------- 1 | import { memoizeByTick } from 'utils/memoizeFunction'; 2 | 3 | export const viz = memoizeByTick( 4 | (room?: string) => room ?? '', 5 | (room?: string) => new RoomVisual(room) 6 | ); 7 | -------------------------------------------------------------------------------- /src/Strategy/ResourceAnalysis/Selectors.ts: -------------------------------------------------------------------------------- 1 | export const powerBankReport = (office: string, id: Id) => 2 | Memory.offices[office].powerbanks.find(r => r.id === id); 3 | -------------------------------------------------------------------------------- /src/Strategy/constants.ts: -------------------------------------------------------------------------------- 1 | export const ACQUIRE = { 2 | SCORE_WEIGHT: { 3 | RAMPART_COUNT: 1, 4 | FRANCHISES_INSIDE_PERIMETER: 1, 5 | SWAMP_COUNT: 1, 6 | MINERAL_TYPE: 2, 7 | REMOTE_COUNT: 1, 8 | DISTANCE: 3 9 | }, 10 | MINERAL_PRIORITIES: [ 11 | RESOURCE_CATALYST, 12 | RESOURCE_UTRIUM, 13 | RESOURCE_KEANIUM, 14 | RESOURCE_LEMERGIUM, 15 | RESOURCE_ZYNTHIUM, 16 | RESOURCE_HYDROGEN, 17 | RESOURCE_OXYGEN 18 | ] as MineralConstant[] 19 | }; 20 | -------------------------------------------------------------------------------- /src/Structures/Labs.ts: -------------------------------------------------------------------------------- 1 | import { runLabLogic } from "./Labs/Labs"; 2 | import { planLabOrders } from "./Labs/planLabOrders"; 3 | 4 | export const runLabs = () => { 5 | for (let office in Memory.offices) { 6 | planLabOrders(office); 7 | runLabLogic(office); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Structures/Labs/LabOrder.ts: -------------------------------------------------------------------------------- 1 | export interface LabOrder { 2 | ingredient1: LabMineralConstant; 3 | ingredient2: LabMineralConstant; 4 | output: LabMineralConstant; 5 | amount: number; 6 | } 7 | export interface BoostOrder { 8 | name: string; 9 | boosts: { type: MineralBoostConstant; count: number }[]; 10 | } 11 | 12 | export type LabMineralConstant = MineralCompoundConstant | MineralConstant; 13 | 14 | declare global { 15 | interface OfficeMemory { 16 | lab: { 17 | orders: LabOrder[]; 18 | boosts: BoostOrder[]; 19 | boostingLabs: { 20 | id: Id; 21 | resource: MineralBoostConstant; 22 | }[]; 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Structures/Labs/labsToEmpty.ts: -------------------------------------------------------------------------------- 1 | import { getLabs } from 'Selectors/getLabs'; 2 | import { LabOrder } from './LabOrder'; 3 | 4 | export function boostLabsToEmpty(office: string) { 5 | return getLabs(office) 6 | .boosts.map(lab => lab.structure) 7 | .filter((lab): lab is StructureLab => { 8 | if (!lab) return false; 9 | const target = Memory.offices[office].lab.boostingLabs.find(o => o.id === lab.id)?.resource; 10 | const actual = lab.mineralType; 11 | return Boolean(actual && actual !== target); 12 | }); 13 | } 14 | 15 | export function reactionLabsToEmpty(office: string) { 16 | const order = Memory.offices[office].lab.orders.find(o => o.amount > 0) as LabOrder | undefined; 17 | const { inputs, outputs } = getLabs(office); 18 | const outputLabs = outputs.map(s => s.structure).filter((s): s is StructureLab => !!s?.mineralType); 19 | const [lab1, lab2] = inputs.map(s => s.structure); 20 | 21 | const labs = outputLabs; 22 | if ( 23 | lab1?.mineralType && 24 | (lab1.mineralType !== order?.ingredient1 || (lab1?.store.getUsedCapacity(lab1.mineralType) ?? 0) < 5) 25 | ) 26 | labs.push(lab1); 27 | if ( 28 | lab2?.mineralType && 29 | (lab2?.mineralType !== order?.ingredient2 || (lab2?.store.getUsedCapacity(lab2.mineralType) ?? 0) < 5) 30 | ) 31 | labs.push(lab2); 32 | return labs; 33 | } 34 | -------------------------------------------------------------------------------- /src/Structures/Labs/planLabOrders.ts: -------------------------------------------------------------------------------- 1 | import { boostQuotas } from 'Selectors/boostQuotas'; 2 | import { roomPlans } from 'Selectors/roomPlans'; 3 | import { getLabOrders } from './getLabOrderDependencies'; 4 | 5 | export function planLabOrders(office: string) { 6 | // Prune completed orders 7 | const terminal = roomPlans(office)?.headquarters?.terminal.structure as StructureTerminal | undefined; 8 | if (!terminal) return; 9 | 10 | // Maintain quotas 11 | if (Memory.offices[office].lab.orders.some(o => o.amount <= 0)) { 12 | Memory.offices[office].lab.orders = []; // reset after each lab order is completed 13 | } 14 | if (Memory.offices[office].lab.orders.length === 0) { 15 | for (const { boost, amount } of boostQuotas(office)) { 16 | const difference = amount - terminal.store.getUsedCapacity(boost); 17 | if (difference > 0) { 18 | try { 19 | Memory.offices[office].lab.orders.push(...getLabOrders(boost, difference, terminal)); 20 | break; 21 | } catch { 22 | // No market and not enough ingredients 23 | continue; 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Structures/Links.ts: -------------------------------------------------------------------------------- 1 | import { estimatedFreeCapacity, estimatedUsedCapacity, updateUsedCapacity } from 'Selectors/Logistics/predictiveCapacity'; 2 | import { roomPlans } from 'Selectors/roomPlans'; 3 | 4 | export const runLinks = () => { 5 | for (let office in Memory.offices) { 6 | const plan = roomPlans(office); 7 | 8 | const hqlink = plan?.headquarters?.link.structure as StructureLink; 9 | const franchise1link = plan?.franchise1?.link.structure as StructureLink | undefined; 10 | const franchise2link = plan?.franchise2?.link.structure as StructureLink | undefined; 11 | const fastfillerlink = plan?.fastfiller?.link.structure as StructureLink | undefined; 12 | const librarylink = plan?.library?.link.structure as StructureLink | undefined; 13 | 14 | const destinations = [fastfillerlink, librarylink, hqlink] 15 | .filter((l): l is StructureLink => estimatedFreeCapacity(l) > LINK_CAPACITY / 2) 16 | .sort((a, b) => estimatedFreeCapacity(a) - estimatedFreeCapacity(b)); 17 | 18 | for (const source of [ 19 | franchise1link, 20 | franchise2link, 21 | hqlink 22 | ].filter(l => estimatedUsedCapacity(l) > CARRY_CAPACITY)) { 23 | if (source && estimatedUsedCapacity(source) && !source.cooldown) { 24 | const destination = destinations.filter(d => d !== source).shift(); 25 | if (destination) { 26 | if (source.transferEnergy(destination) === OK) { 27 | const transfer = Math.min(estimatedUsedCapacity(source), estimatedFreeCapacity(destination)); 28 | updateUsedCapacity(source, -transfer); 29 | updateUsedCapacity(destination, transfer * 0.97); 30 | if (estimatedFreeCapacity(destination)) destinations.unshift(destination); 31 | } 32 | } 33 | } 34 | } 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/Structures/PowerSpawn.ts: -------------------------------------------------------------------------------- 1 | import { roomPlans } from 'Selectors/roomPlans'; 2 | 3 | export const runPowerSpawn = () => { 4 | for (const office in Memory.offices) { 5 | const powerSpawn = roomPlans(office)?.headquarters?.powerSpawn.structure as StructurePowerSpawn | undefined; 6 | powerSpawn?.processPower(); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /src/Structures/Ramparts.ts: -------------------------------------------------------------------------------- 1 | import { totalCreepStats } from 'Selectors/Combat/combatStats'; 2 | import { rampartsAreBroken } from 'Selectors/Combat/defenseRamparts'; 3 | import { findAlliedCreeps } from 'Selectors/findAlliedCreeps'; 4 | import { findHostileCreeps } from 'Selectors/findHostileCreeps'; 5 | import { roomPlans } from 'Selectors/roomPlans'; 6 | 7 | const rampartState = new Map(); 8 | 9 | export const runRamparts = () => { 10 | for (const room in Memory.offices) { 11 | rampartState.set( 12 | room, 13 | roomPlans(room)?.perimeter?.ramparts.some(r => (r.structure as StructureRampart)?.isPublic) ?? false 14 | ); 15 | if (findHostileCreeps(room).length) { 16 | if (rampartState.get(room)) 17 | roomPlans(room)?.perimeter?.ramparts.forEach(r => (r.structure as StructureRampart)?.setPublic(false)); 18 | } else if (findAlliedCreeps(room).length) { 19 | if (!rampartState.get(room)) 20 | roomPlans(room)?.perimeter?.ramparts.forEach(r => (r.structure as StructureRampart)?.setPublic(true)); 21 | } 22 | 23 | // auto safe mode if ramparts are broken and we have hostile creeps or no tower 24 | const hostileScore = totalCreepStats(findHostileCreeps(room), true).score; 25 | if ( 26 | rampartsAreBroken(room) && 27 | !Game.rooms[room].controller?.safeMode && 28 | Game.rooms[room].controller?.safeModeAvailable && 29 | (hostileScore > 30 || (hostileScore > 0 && roomPlans(room)?.backfill?.towers.every(t => !t.survey()))) 30 | ) { 31 | Game.rooms[room].controller?.activateSafeMode(); 32 | } 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/Structures/Towers.ts: -------------------------------------------------------------------------------- 1 | import { HEAL_RANGE, RANGED_HEAL_RANGE } from 'gameConstants'; 2 | import { findHostileCreeps, findHostileCreepsInRange } from 'Selectors/findHostileCreeps'; 3 | import { roomPlans } from 'Selectors/roomPlans'; 4 | import { towerDamage } from 'Selectors/towerDamage'; 5 | 6 | export const runTowers = () => { 7 | for (let office in Memory.offices) { 8 | const plan = roomPlans(office); 9 | if (!plan?.backfill || !Game.rooms[office]) return; 10 | 11 | // Count active towers 12 | 13 | // Select the target that will take the most damage 14 | const targets = findHostileCreeps(office); 15 | let priorityTarget: Creep | undefined = undefined; 16 | let bestDamage = 0; 17 | for (let target of targets) { 18 | const damage = plan.backfill.towers.reduce( 19 | (sum, t) => sum + towerDamage(t.structure as StructureTower | undefined, target.pos), 20 | 0 21 | ); 22 | const exitRange = target.pos.findClosestByRange(FIND_EXIT)?.getRangeTo(target) ?? 50; 23 | const selfHeal = target.getActiveBodyparts(HEAL) * HEAL_POWER; 24 | const allyHeal = findHostileCreepsInRange(target.pos, RANGED_HEAL_RANGE).reduce((sum, ally) => { 25 | return ( 26 | sum + 27 | ally.getActiveBodyparts(HEAL) * (ally.pos.inRangeTo(target.pos, HEAL_RANGE) ? HEAL_POWER : RANGED_HEAL_POWER) 28 | ); 29 | }, 0); 30 | const netDamage = exitRange > 2 ? damage - (selfHeal + allyHeal) : 0; // Assume creeps within range of an exit will escape for healing 31 | if (netDamage > bestDamage) { 32 | priorityTarget = target; 33 | bestDamage = netDamage; 34 | } 35 | } 36 | 37 | // Attack the target, if found 38 | if (priorityTarget) { 39 | for (let t of plan.backfill.towers) { 40 | if (!t.structure) continue; 41 | (t.structure as StructureTower).attack(priorityTarget); 42 | } 43 | } 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/Structures/index.ts: -------------------------------------------------------------------------------- 1 | import { runLabs } from './Labs'; 2 | import { runLinks } from './Links'; 3 | import { runObserver } from './Observer'; 4 | import { runPowerSpawn } from './PowerSpawn'; 5 | import { runRamparts } from './Ramparts'; 6 | import { runTerminal } from './Terminal'; 7 | import { runTowers } from './Towers'; 8 | 9 | export const runStructures = () => { 10 | runTowers(); 11 | runLinks(); 12 | runTerminal(); 13 | runLabs(); 14 | runObserver(); 15 | runPowerSpawn(); 16 | runRamparts(); 17 | }; 18 | -------------------------------------------------------------------------------- /src/TaskManager.ts: -------------------------------------------------------------------------------- 1 | import { logCpu, logCpuStart } from "utils/logCPU"; 2 | 3 | interface Task { 4 | name: string; 5 | fn: () => void; 6 | mandatory?: boolean; 7 | runEvery?: number; 8 | threshold?: number; 9 | } 10 | 11 | const lastRun = new Map(); 12 | 13 | export function runTaskManager(tasks: Task[], cpuLimit: number, debug = false) { 14 | const start = Game.cpu.getUsed(); 15 | if (debug) logCpuStart(); 16 | for (const task of tasks) { 17 | if (!task.mandatory && Game.cpu.getUsed() - start > cpuLimit) { 18 | if (debug) console.log(Game.time, 'skipping task', task.name); 19 | continue; 20 | } 21 | if (task.threshold && Game.cpu.bucket < task.threshold) { 22 | if (debug) console.log(Game.time, 'skipping task', task.name, 'due to low bucket'); 23 | continue; 24 | } 25 | if ( 26 | (!task.runEvery || (lastRun.get(task.name) ?? 0) + task.runEvery < Game.time) 27 | ) { 28 | task.fn(); 29 | if (debug) logCpu(task.name); 30 | lastRun.set(task.name, Game.time); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | // example declaration file - remove these and add your own custom typings 2 | 3 | // From https://github.com/screepers/RoomVisual 4 | interface RoomVisual { 5 | structure(x: number, y: number, structureType: StructureConstant): RoomVisual 6 | speech(text: string, x: number, y: number): RoomVisual 7 | animatedPosition(x: number, y: number): RoomVisual 8 | resource(type: ResourceConstant, x: number, y: number): RoomVisual 9 | connectRoads(): RoomVisual 10 | } 11 | 12 | interface RawMemory { 13 | _parsed: Memory 14 | } 15 | 16 | interface RoomTerrain { 17 | getRawBuffer(): Uint8Array 18 | } 19 | 20 | interface OfficeMemory { 21 | city: string, 22 | resourceQuotas: Partial>, 23 | } 24 | 25 | interface Memory { 26 | offices: { 27 | [name: string]: OfficeMemory 28 | } 29 | } 30 | 31 | declare namespace GreyCompany { 32 | interface Heap { } 33 | } 34 | // `global` extension samples 35 | declare namespace NodeJS { 36 | interface Global { 37 | log: any; 38 | purge: Function; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/CityNames.ts: -------------------------------------------------------------------------------- 1 | export const cityNames = [ 2 | "Tokyo", 3 | "Delhi", 4 | "Shanghai", 5 | "São Paulo", 6 | "Mexico City", 7 | "Cairo", 8 | "Mumbai", 9 | "Dhaka", 10 | "Osaka", 11 | "New York City", 12 | "Karachi", 13 | "Buenos Aires", 14 | "Istanbul", 15 | "Kolkata", 16 | "Manila", 17 | "Lagos", 18 | "Rio de Janeiro", 19 | "Kinshasa", 20 | "Los Angeles", 21 | "Moscow", 22 | "Lahore", 23 | "Bangalore", 24 | "Paris", 25 | "Bogotá", 26 | "Jakarta", 27 | "Chennai", 28 | "Lima", 29 | "Bangkok", 30 | "Seoul", 31 | "Nagoya", 32 | "Hyderabad", 33 | "London", 34 | "Tehran", 35 | "Chicago", 36 | "Ho Chi Minh City", 37 | "Luanda", 38 | "Ahmedabad", 39 | "Kuala Lumpur", 40 | "Riyadh", 41 | "Baghdad", 42 | "Santiago", 43 | "Surat", 44 | "Madrid", 45 | "Pune", 46 | "Houston", 47 | "Dallas", 48 | "Toronto", 49 | "Dar es Salaam", 50 | "Miami", 51 | "Belo Horizonte", 52 | "Singapore", 53 | "Philadelphia", 54 | "Atlanta", 55 | "Fukuoka", 56 | "Khartoum", 57 | "Barcelona", 58 | "Johannesburg", 59 | "Saint Petersburg", 60 | "Washington, D.C.", 61 | "Yangon", 62 | "Alexandria", 63 | "Guadalajara" 64 | ] 65 | -------------------------------------------------------------------------------- /src/utils/cleanUpCreeps.ts: -------------------------------------------------------------------------------- 1 | export function cleanUpCreeps() { 2 | for (const k in Memory.creeps) { 3 | if (!Game.creeps[k]) { 4 | delete Memory.creeps[k]; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/debugCPU.ts: -------------------------------------------------------------------------------- 1 | let start = 0; 2 | let current = 0; 3 | let saveInMemory = false; 4 | 5 | export const resetDebugCPU = (inMemory = false) => { 6 | saveInMemory = inMemory; 7 | start = Game.cpu.getUsed(); 8 | current = start; 9 | if (!inMemory) { 10 | console.log(` -=< Starting CPU debug >=- [ 0.000 | 0.000 ]`); 11 | } else { 12 | Memory.stats.profiling = {}; 13 | } 14 | }; 15 | export const debugCPU = (context: string) => { 16 | let previous = current; 17 | current = Game.cpu.getUsed(); 18 | if (!saveInMemory) { 19 | console.log(`${context.padEnd(35)} [ ${(current - previous).toFixed(3)} | ${(current - start).toFixed(3)} ]`); 20 | } else { 21 | if (!Memory.stats) return; 22 | Memory.stats.profiling ??= {}; 23 | Memory.stats.profiling[context] = (Memory.stats.profiling[context] ?? 0) + current - previous; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/utils/dump.ts: -------------------------------------------------------------------------------- 1 | export function dump(value: T): T { 2 | // parse stack into a single line without line numbers 3 | const stackRegex = /at\s+([^\s]+)\s/i; 4 | const stack = new Error().stack?.split('\n').map(l => { 5 | const match = l.match(stackRegex); 6 | return match ? match[1].trim() : undefined; 7 | }) 8 | .filter((l): l is string => !!l && !l.includes('dump')) 9 | .reverse(); 10 | if (stack?.includes("Object.wrap")) { 11 | stack.splice(0, stack.indexOf("Object.wrap") + 1); 12 | } 13 | const context = stack?.join(' > '); 14 | 15 | // log stack and value 16 | console.log(context + "\n" + JSON.stringify(value, null, 2)) 17 | 18 | return value; 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/initializeSpawns.ts: -------------------------------------------------------------------------------- 1 | import { getSpawns, roomPlans } from 'Selectors/roomPlans'; 2 | 3 | let done = false; 4 | 5 | /** 6 | * Initializes first spawn for autoplacement in private servers 7 | */ 8 | export function initializeSpawn() { 9 | if (done) return; 10 | const offices = Object.keys(Memory.offices); 11 | if (offices.length === 1 && !getSpawns(offices[0]).length) 12 | if (roomPlans(offices[0])?.fastfiller?.spawns[0].pos.createConstructionSite(STRUCTURE_SPAWN) === OK) { 13 | // place initial spawn site 14 | done = true; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/logCPU.ts: -------------------------------------------------------------------------------- 1 | let log = new Map(); 2 | let loggedTicks = 0; 3 | let last = 0; 4 | let reportedTick = 0; 5 | 6 | export const logCpuStart = () => (last = Game.cpu.getUsed()); 7 | export const logCpu = (context: string) => { 8 | if (reportedTick !== Game.time) { 9 | console.log('--- logCpu ---'); 10 | for (let [c, data] of log) { 11 | const invocationsPerTick = data[0] / loggedTicks; 12 | const averagePerInvocation = data[1] / data[0]; 13 | console.log( 14 | `${c}: ${invocationsPerTick.toFixed(3)} x ${averagePerInvocation.toFixed(3)} = ${( 15 | invocationsPerTick * averagePerInvocation 16 | ).toFixed(3)}` 17 | ); 18 | } 19 | loggedTicks += 1; 20 | reportedTick = Game.time; 21 | } 22 | const [invocations, time] = log.get(context) ?? [0, 0]; 23 | const cpu = Game.cpu.getUsed(); 24 | log.set(context, [invocations + 1, time + Math.max(0, cpu - last)]); 25 | last = cpu; 26 | }; 27 | -------------------------------------------------------------------------------- /src/utils/memhack.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface Global { 4 | Memory?: Memory 5 | } 6 | } 7 | } 8 | 9 | // Usage: 10 | // At top of main: import MemHack from './MemHack' 11 | // At top of loop(): MemHack.pretick() 12 | // Thats it! 13 | const MemHack = { 14 | memory: undefined as (Memory|undefined), 15 | parseTime: -1, 16 | register () { 17 | const start = Game.cpu.getUsed() 18 | this.memory = Memory 19 | const end = Game.cpu.getUsed() 20 | this.parseTime = end - start 21 | this.memory = RawMemory._parsed 22 | }, 23 | pretick () { 24 | if (this.memory) { 25 | delete global.Memory 26 | global.Memory = this.memory 27 | RawMemory._parsed = this.memory 28 | } 29 | }, 30 | } 31 | MemHack.register() 32 | 33 | export default MemHack 34 | -------------------------------------------------------------------------------- /src/utils/visualizeCostMatrix.ts: -------------------------------------------------------------------------------- 1 | import { viz } from 'Selectors/viz'; 2 | import { memoizeByTick } from './memoizeFunction'; 3 | 4 | export const visualizeCostMatrix = memoizeByTick( 5 | (cm, room) => room, 6 | (cm: CostMatrix, room: string) => { 7 | for (let x = 0; x < 100; x++) { 8 | for (let y = 0; y < 100; y++) { 9 | if (cm.get(x, y)) viz(room).text(cm.get(x, y).toString(), x, y, { font: '0.4' }); 10 | } 11 | } 12 | } 13 | ); 14 | -------------------------------------------------------------------------------- /src/utils/visualizeRoomCluster.ts: -------------------------------------------------------------------------------- 1 | import { memoize } from './memoizeFunction'; 2 | 3 | export const visualizeRoomCluster = (rooms: string[], opts?: LineStyle) => { 4 | // construct border 5 | bordersFromRooms(rooms).forEach(([pos1, pos2]) => Game.map.visual.line(pos1, pos2, opts)); 6 | }; 7 | 8 | const bordersFromRooms = memoize( 9 | (rooms: string[]) => rooms.join(), 10 | (rooms: string[]) => { 11 | const borders: [RoomPosition, RoomPosition][] = []; 12 | 13 | const boundaries = rooms.slice(); 14 | while (boundaries.length) { 15 | const room = boundaries.shift()!; 16 | 17 | const exits = Game.map.describeExits(room); 18 | if (!rooms.includes(exits[LEFT]!)) { 19 | borders.push([new RoomPosition(0, 0, room), new RoomPosition(0, 49, room)]); 20 | } 21 | if (!rooms.includes(exits[RIGHT]!)) { 22 | borders.push([new RoomPosition(49, 0, room), new RoomPosition(49, 49, room)]); 23 | } 24 | if (!rooms.includes(exits[TOP]!)) { 25 | borders.push([new RoomPosition(0, 0, room), new RoomPosition(49, 0, room)]); 26 | } 27 | if (!rooms.includes(exits[BOTTOM]!)) { 28 | borders.push([new RoomPosition(0, 49, room), new RoomPosition(49, 49, room)]); 29 | } 30 | } 31 | return borders; 32 | } 33 | ); 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "lib": ["esnext"], 5 | "target": "ES2017", 6 | "moduleResolution": "Node", 7 | "outDir": "dist", 8 | "baseUrl": "src/", 9 | "sourceMap": true, 10 | "strict": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "noImplicitReturns": true, 14 | "allowSyntheticDefaultImports": true, 15 | "allowUnreachableCode": false, 16 | "downlevelIteration": true 17 | }, 18 | "exclude": ["node_modules"] 19 | } 20 | --------------------------------------------------------------------------------