Screeps: like Starcraft at 0.001% speed
If you knew me in early 2017, you probably knew I became entirely too invested writing an AI for the programming strategy game Screeps. (If you didn’t know me in early 2017, it’s probably because I was hiding in my room programming.)
Since I started playing the game – exactly 11 months ago as of today – I’ve created a Screeps AI, with some ~50,000 additions to the GitHub repository, which I dubbed Overmind (much of the code is vaguely Starcraft-themed). I stopped playing the game for a few months over the last summer, but I was recently surprised to find my code was still running like a well-oiled machine on the public servers, so I decided to pick the game back up again. There’s a lot of changes and improvements I’d like to make, and I’ve enjoyed reading other people’s blogs about their evolving Screeps AI’s, so I’ve decided to start my own.
Overmind: a brief history
Any large programming project has an iterative write-deploy-rewrite rhythm to it, but some rewrites are larger than others. I can break down the history of my Screeps AI to five stages, separated by the largest rewrites:
- The second version (first rewrite) refactored the roles into extensible classes and added a collective
RoomBrainobject to prioritize tasks and assign them to creeps in the most efficient order possible, functioning a little bit like a greedy scheduling algorithm dispatching processes in an OS. It also added
Taskobjects to more concisely tell creeps what to do and when to stop doing it, and it added the first instances of behavior modifications and military capabilities, with flags functioning as injectable code snippets to modify the standard behavior.
- In version 3, I rewrote the entire AI using TypeScript, after my ballooning codebase began to become riddled with runtime errors which could easily be fixed by a strong type system. I found structNullChecks to be especially valuable, since many of the errors I was getting were when I was unsafely trying to access a nonexistent object. Migrating to TypeScript changed the general flavor of much of my code, and my AI became super object-oriented in the next rewrite.
- The fourth version of the AI is where I left off over last summer. As I was expanding quickly, I needed a stronger and more flexible way to deal with objects across multiple rooms. So, I introduced Colonies to group together outposts operated by a single owned room and Hive Clusters to group creeps and structures with a similar purpose (for example, spawning structures in a
Hatchery). Although colonies greatly simplified the process of programming just about everything (instead of finding sources across multiple rooms filtered rooms, you can just say colony.sources), it added significant CPU overhead, so I added a more advanced preprocessing system to avoid doing duplicate searches in the same tick. I also added
Objectives, which dispense tasks when assigned to a creep. To more finely control which creeps can do what actions, allowing for dedicated creeps to attend to certain clusters, I added
ResourceRequestGroups, which allow you to group together some subset of the operations that need to be done within a colony, so that only certain creeps will access them. (For example, only the hatchery attendants should refill the hatchery extensions.)
- In the current version of the AI, I haven’t made any radical changes yet, but I’ve began experimenting with some new features, such as
Directives, which represent a more advanced method of behavior modification, and automatic room planning. Slowly but surely, I think I’m beginning to decide on a concrete direction to take the next rewrite.
The good, the bad, and the ugly
Over the last few weeks, I’ve spent a lot of time thinking about what I like and what I don’t like in my Screeps AI. Here’s a few of the things it does well:
- Colonies effectively abstractify away distinctions in rooms, making it very easy to program multi-room operations, such as container mining and hauling.
- Hive Clusters are an intuitive way to program related functionalities that offers a lot of fine control over the behavior of the creeps attending to the cluster. They are also good at designating specific functionalities for specific structures. For example, the Hatchery link, which always requests energy, is just
colony.hatchery.link, and the Command Center link is
colony.commandCenter.link, which is managed by
- Tasks are generally pretty good and minimize much of the decision-tree overhead that many AI’s feature, since a creep only needs to request a new task when its old one becomes invalid. While they are good at most actions, they aren’t the most flexible design, so they are bad for more complex scenarios where multiple actions should be executed, such as healing and attacking at the same time while trying to maintain a certain range. I’ve added a few things to make them more process-like, including forking tasks and reverting to parent tasks, but they’re probably only good for about 80% of use cases.
- Directives are still pretty simple and limited in scope, but I think they are a pretty promising way to implement conditional behavior modification.
- My room planning system is gonna be really cool when I finish it! (More about that in a future post.)
Now for the things I don’t like:
- Between Roles and Hive Clusters, I’ve developed two very different programming paradigms for controlling creep actions. I generally prefer the latter, since the former can be pretty inflexible when coordinating multiple roles, but it’s obviously not suitable for all cases. However, I think there needs to be a single unified method for programming creep actions that combines the best of both paradigms.
- Because my AI instantiates so many book-keeping objects with Colonies and Hive Clusters, the initialization phase is really sensitive to the order in which you call things, since all of these objects can be pretty tightly interlinked.
- My AI sucks at low RCL, especially if I don’t have a grown colony to incubate the new one. This is mainly because I haven’t respawned since the very first rewrite of my AI, so I haven’t put much thought into how low-level code works. However, it’s also caused by bugs in the next bullet point.
- I like the idea of ObjectiveGroups, but I think the Objectives are an overly-complicated and buggy system. The way that the ObjectiveGroup dispenses tasks doesn’t offer very much intuition or control over what’s going on, and since objectives and tasks are closely related but different, it is possible for a creep to lose the task but still think it’s associated with the corresponding objective. This can cause fatal bugs at low RCL.
- My spawning code is pretty clean, but it gets called in many different locations. Hive clusters request creeps, Directives conditionally request creeps, and the colony
Overlordrandomly requests the remaining creeps. I want to consolidate the spawning requests to a single process.
- I’ve spent so much time writing and rewriting the core framework of how my AI functions that I haven’t added entire sets of features to it. I’m almost done with room planning, but I still have very minimal military code and little to no trading or mineral processing capabilities. I’m hoping that this next rewrite will begin to set the final paradigm of how things are done in Overmind and that I can progress to adding new features to finish the AI.
A rewrite and a respawn
Now that my first term of grad school is over, I finally have some time on my hands to make some major changes. I have a relatively solid idea of the general direction I’d like to take things, but I won’t give away too much:
- I think my first priority is rewriting the way I control creeps. I like how Hive Clusters control them, so I think I’m going to extract that creep control method along with all of the spawning requests scattered around, which follow a very similar pattern to each other, and put then in a new class:
Overseer. I’m still figuring out exactly how this will work, but my thought is to tether creep control and spawning requests to a process that needs to be done, such as building construction sites, remote mining, or even creep-less processes such as managing link transfers. I want to make Overseers integrate cleanly with Hive Clusters but also make them independent so that structure-less or conditional operations, such as guarding a room from invaders, can be programmed using Overseers too.
- Currently, I have two main phases in each tick:
run(). I want to refactor the AI initialization by adding a
build()phase (which I’ve already started working on), so that it doesn’t matter which order you instantiate components in.
- Instantiating all of the book-keeping objects can be pretty CPU intensive. I recently read BonzAI’s post where he’s started creating global objects that last between ticks. I think this might be even more useful in my case, since I basically rebuild the entire game environment with every tick. I’ll probably split
rebuild(). The former will get called once per node every global reset, and the latter will conditionally re-cache certain properties of objects.
I’ve made a ton of changes over the last few weeks preparing for this rewrite, and not all of them have gone over smoothly. I’ve introduced several game-breaking bugs that have cause colony-wide blackouts, and now that I’ve started doing most of my testing on a private server, I’ve started to neglect my public server code. I’ve also become kind of lonely with the release of shards, since a good portion of shard0 has moved to the faster and more densely-packed shard1 and shard2. After I finish this rewrite, I think I’ll try my luck and respawn in one of the new shards with my shiny new Screeps AI.