An AI created for the game Screeps
This repository contains the code I am using for the excellent, unique game Screeps, which has similar design to a real-time strategy, but where the player must write JavaScript* to control their structures and units. There is no direct 'point and click' interface.
Screeps means “scripting creeps.” It’s an open-source sandbox MMO RTS game for programmers, wherein the core mechanic is programming your units’ AI. You control your colony by writing JavaScript which operate 24/7 in the single persistent real-time world filled by other players on par with you.
The codebase is written entirely in TypeScript, and uses the starter package, linked below.
Currently the code is focused primarily on economy and room management, with some very basic military code for defense. As these features are flushed out, the software will expand into other areas such as automated claiming, market activities, more sophisticated military operations, etc. Following is a loosely organized description of the software, written primarily to provide myself with some concrete reference of how everything works, and help create some design guidelines.
*Or any language that compiles to JavaScript. TypeScript is a particularly popular choice, providing static type checking with JavaScript like syntax, and has a starter package specifically for Screeps available.
There are two primary parts of the project, lib
rary and imp
lementation. lib
consists of classes which are either utility focused, abstract, or depend upon some concrete implementation of an abstract class. This is essentially the framework of the AI, which could, in theory, be used in other projects. It follows several conventions outlined below in link Architecture. imp
is where the implementation of those abstract classes defined in lib
live. It is here that you will find the meat of the logic, such as creep controllers, operations, etc.
To fully understand the architecture of Black Cat requires some knowledge of how Screeps works. The Screeps server uses Node.js as its execution engine, and user code is executed from within a virtual machine. When the virtual machine executing your program spins up for the first time, it evaluates your code, looks for a function called
loop()
inside a file namedmain.js
, and calls it. The results of the evaluation are cached, such that on all future ticks executing under the same context (same VM), it simply callsloop()
again, without re-evaluating the code.There is no guarantee that you will have the same VM from any one tick to the next, a change to your code, a memory leak, or some other condition may cause the destruction of your VM and require assignment to a new one. There are several implications to this, the primary of which is the loss of the heap. Since your volatile memory can be lost between any two ticks, Screeps provides a mechanism for serializing and storing data in a
Memory
object, which must be used to persist any information critical to execution.Additionally, all objects provided by the Screeps API (game objects) are reinstantiated each tick, so will be stale if not refreshed. This fact, combined with the nature of the VM, heavily informs the way Black Cat is designed.
The entry point, main.js
, is very simple. Outside the main loop it extends the global object with some constants (more on that below) instantiates an Execute
object, and calls execute.init()
on it. Inside the loop, execute.main()
is called, which drives the AI.
Inside execute.init()
there are some memory management tasks (to ensure good structure), some additional global extensions for convenience and debugging purposes, and registrations for a variety of objects (again, more on this below).
The execute.main()
method creates all objects from scratch each tick, which is a decision informed by the nuances of Screeps (possibly subject to change with the introduction of a stable Isolated Virtual Machine), by creating an Empire
object, which is the top-level for all other logic. The Empire
object then drives the lower-level objects using the execution cycle described below.
Given the nature of Screeps, it is required that we persist all state data each tick, and it is important to manage this process carefully. You will see the following pattern in nearly every class in this project.
All classes which require persistent data provide these three things:
An interface describing their memory
interface FooMemory { ... }
A method for getting that data
save(): FooMemory { ... }
A static method for creating an instance from memory (or hydrating if abstract)
Foo.fromMemory(memory: FooMemory): Foo { ... }
|| Bar.fromMemory(memory: BarMemory, instance: Bar): Bar { ... }
.
Here is a more complete example illustrating the memory pattern:
export abstract class Foo {
public static fromMemory(memory: FooMemory, instance: Foo): Foo {
instance.quantity = memory.quantity;
return instance;
}
public quantity: number;
public save(): FooMemory {
return {
quantity: this.quantity
};
}
}
export interface FooMemory {
quantity: number;
}
export class Bar extends Foo {
public static fromMemory(memory: BarMemory): Bar {
let bar = new this();
bar.type = memory.type;
return Foo.fromMemory(memory, bar) as Bar;
}
public type: string;
public save(): BarMemory {
let mem = super.save() as BarMemory;
mem.type = this.type;
return mem;
}
}
export interface BarMemory extends FooMemory {
type: string;
}
export class Consumer {
public loop(): void {
let bar = Bar.fromMemory(Memory.bar);
...
Memory.bar = bar.save();
}
}
Execution starts with an initialization phase, which is triggered by a patch, VM switch, or some other event which causes the loss of volatile memory. During the initialization phase, memory is checked, prototypes are extended, constants are defined, and creation/hydration methods are registered with their repositories.
After the initialization phase, the main loop begins, which will execute repeatedly until an event triggers another initialization. The main loop executes 5 methods, in order, on all objects. Load()
-> Update()
-> Execute()
-> Cleanup()
-> Save()
.
Load()
gives a class the chance to get any game objects it needs, by way of Game.getObjectById
. Also helps to avoid circular dependencies in some cases.
Update()
allows a class to collect data and update its state.
Execute()
is where the main logic of the class is executed.
Cleanup()
provides an opportunity for post execution data gathering and clean up.
Save()
persist data back to the memory object.
This pattern is applied throughout the project, but is not enforced by an interface or system of any sort. Some classes take parameters in these methods. This may eventually be refactored so that they can all be treated equally, which may lend itself to porting into an OS style system.
The Empire
is the highest level control unit, and constitutes the entirety of a player's Screeps activities. It controls and directs from a birds-eye view, identifying new potential Colonies
and informing existing Colonies
of suggested plans and marketing actions.
Colonies
are the main control units for rooms
and economic activities. They consist of a nest
, the main room
of the colony
, explained in further detail below, and expansions
, which are remote mining rooms. There is also a population
object which allows easy analysis of creeps. A colony tracks its state using a progress
object, which has a collection of milestones
. When a milestone
is met, it raises a flag, and other interested parties may react to that flag. colonyPlans
are one of those parties, which are responsible for creating and managing operations
.
The nest
is the core of a colony
. A nest
, technically speaking, is any room
which has a spawn
, each nest
belongs to exactly one colony
and each colony
has exactly one nest
. All spawning is handled by the nest
which distributes the work to one of its spawners
, an object which wraps the native spawn
. The nest uses a nestMap
, which is generated with the colony
, to determine when and where to build structures, important locations, cost matrices, etc.
An operation
groups creeps
to perform a focused task, such as building a set of structures, harvesting energy
from a source, upgrading a controller, repairing roads, etc. An operation
has four phases, initial
, initialized
, started
, and finished
. It defines when it is available to advance to each phase. Additionally, it defines its creep
requirements, and once enough have been assigned can be started. After starting, it controls its assigned creeps
directly, or by executing their role
- either way, the operation
is responsible for managing the creeps
that have been assigned to it. To be clear, when execute()
is called on the operation
, it 'ticks' all of its creeps
. All creeps
are managed through operations
.
An operation group
manages one or more operations
, and all operations
are executed through an operation group
. It tracks the phase of each operation
, performing checks and updates as necessary. It is also responsible for spawning creeps
for the operations
. By checking with each operation
for its requirements, it calls back to the colony
that is currently managing the group and asks for spawns.
- Repair Structures
- Repair Roads
- Repair Walls
- Cleanup Fallen Energy
- Defend Room
- Collapse Recovery
- Basic Maintenance
- First Spawn Construction
- Harvest Container Construction
- Light Upgrade
- Heavy Harvest Container
- RCL2 Extension Construction
- Controller Container Construction
- Heavy Upgrade Container
- First Tower Construction
- RCL3 Extension Construction
- Storage Construction
- RCL4 Extension Construction
- Second Tower Construction
- First Link Set Construction
- Heavy Harvest Link
- Heavy Upgrade Hybrid
- Heavy Upgrade Link
- Energy Transport
- Control [control]
- Spawn [spawn]
- Source Containers [harvestContainers]
- RCL2 [rcl2]
- 5 Extensions [fiveExtensions]
- Upgrade Container [upgradeContainer]
- RCL3 [rcl3]
- First Tower [firstTower]
- RCL4 [rcl4]
- Storage [storage]
- RCL5 [rcl5]
- Second Tower [secondTower]
- First Link Set [firstLinks]
Creeps should be designated to mine from a specific source, and construct a particular container.
- Mine energy from source
- Deliver energy to spawn
- Build container
A single heavy creep should be assigned to a source which it will harvest and transfer to a container. Periodically repair the container.
- Move to source
- Harvest
- Transfer
- Repair
Several light upgraders should ask the Colony to withdraw energy. Use this energy to upgrade the controller.
- Withdraw
- Upgrade
Create sites, construct 5 extensions, requesting energy from the Colony.
- Withdraw
- Build
Create site, build upgrade container.
- Withdraw
- Build
Ask the Colony for Withdraw and Transfer targets.
- Withdraw
- Transfer
The Room is under your control
Start constructing a spawn
.
- First Spawn Construction
A Spawn exists [spawn]
Start constructing source
containers
.
- Harvest Container Construction
Source
containers
exist for each source
in the room
[harvestContainers]
Start harvesting the sources
and upgrading the controller
.
- Heavy Harvest Container
- Heavy Harvest Container
- Light Upgrade
Room
has reached RCL
2 [rcl2]
Construct 5 extensions
, continue to harvest the sources
and upgrade the controller
.
- RCL2 Extension Construction
- Heavy Harvest Container
- Heavy Harvest Container
- Light Upgrade
Room
has 5 extensions
completed [fiveExtensions]
Construct controller
containers
and start transport operations, continue to harvest the sources
and upgrade the controller
.
- Controller Container Construction
- Energy Transport
- Heavy Harvest Container
- Heavy Harvest Container
- Light Upgrade
Room
has upgrade container
completed [upgradeContainer]
Start heavy controller
upgrade using the new container
, continue to harvest the sources
and transport energy
. Cancel the previous controller
upgrade.
- Heavy Upgrade Container
- Energy Transport
- Heavy Harvest Container
- Heavy Harvest Container
Light Upgrade
Room
has reached RCL
3 [rcl3]
Construct first tower
, continue to harvest the sources
, transport energy
, and upgrade the controller
.
- First Tower Construction
- Energy Transport
- Heavy Harvest Container
- Heavy Harvest Container
- Heavy Upgrade Container
Room
has its first tower [firstTower]
Construct 5 additional extensions
, continue to harvest sources
, transport energy
, and upgrade the controller
.
- RCL3 Extension Construction
- Energy Transport
- Heavy Harvest Container
- Heavy Harvest Container
- Heavy Upgrade Container
Room
has reached RCL
4 [rcl4]
Construct storage
, continue to harvest sources
, transport energy
, and upgrade the controller
.
- Storage Construction
- Energy Transport
- Heavy Harvest Container
- Heavy Harvest Container
- Heavy Upgrade Container
Room
has storage
completed [storage]
Construct 10 additional extensions
, continue to harvest sources
, transport energy
, and upgrade the controller
.
- RCL4 Extension Construction
- Energy Transport
- Heavy Harvest Container
- Heavy Harvest Container
- Heavy Upgrade Container
Room
has reached RCL
5 [rcl5]
Construct second tower
, continue to harvest sources
, transport energy
, and upgrade the controller
.
- Second Tower Construction
- Energy Transport
- Heavy Harvest Container
- Heavy Harvest Container
- Heavy Upgrade Container
Room
has second tower
completed [secondTower]
Construct first two links
, continue to harvest sources
, transport energy
, and upgrade the controller
.
- First Link Set Construction
- Energy Transport
- Heavy Harvest Container
- Heavy Harvest Container
- Heavy Upgrade Container
Room
has two links
completed [firstLinks]
Setup hybrid link
upgrader and first link
harvester, continue to harvest a single source
and transport energy
. Cancel one of the container
harvesters, and the container
upgrader.
- Heavy Harvest Link
- Heavy Upgrade Hybrid
- Heavy Harvest Container
Heavy Upgrade ContainerHeavy Harvest Container