Skip to content

Quickstart Part 1: Writing your first mission and operation

bonzaiferroni edited this page Jun 11, 2017 · 17 revisions

Since I wrote this, bonzAI has seen a couple changes that make this a tad outdated. Almost everything works as it did before, but I now use an Agent class as a wrapper for creeps and there are a few other minor changes.

This framework was designed to make it easy to implement new creep behavior. Most of the time, it is as simple as writing a new mission and adding it to an operation. Here is a quick overview of how to think of "missions" and "operations".

  • Missions are where the majority of your game logic will go: creep behavior, tower behavior, terminal behavior, etc.. These define the processes that you want your creeps to carry out.
  • Operations are just a collection of missions. They may do some logic of their own (auto-layout currently happens at the operation level), but mostly they are just a way to organize/spawn missions.

In this tutorial we will write a mission and operation from scratch. In Part 1 we will define HarvestMission and BaseOperation which will implement basic harvester logic. In Part 2 (coming soon) we will make our HarvestMission more interesting by having it harvest from all the sources in the room and carry the energy away. There is already a mission that is ready-to-go that defines all this behavior (MiningMission), but hopefully going through the tutorial will give you an idea about how to best use the existing code and how to start writing your own missions.

Contents

Defining your first mission: HarvestMission

Whenever I want to implement a new game mechanic, I start with writing the creep behavior that I want to support in the mission. After that, working your way backwards to initialize the mission and add it to the operation is a simple matter.

Because your mission extends the abstract class Mission, most IDEs have an option to automatically implement required members, which works really well as a sort of template for getting started. In the gif below I go through the whole process of preparing the file, including writing the constructor.

creating a new mission template

You can pass any information that you want into a mission constructor, but the one required parameter is the Operation that is instantiating it. I'll show you what this looks like in Operation later on. You are also passing the name of the mission into the constructor for Mission. Since all your operations are exposed to global, you can access your mission object through the console with the following command: opName.missions.harvest

If you are curious how Mission continues with the instantiation, you can find it here.

Finding/spawning your creeps with headCount()

If you implemented all your required members like in the gif above, you will now see your phase functions: initMission, roleCall, missionActions, and finalizeMission. They get called every tick in the order you see here. Lets ignore initMission for now and go directly on to roleCall.

All the logic for spawning creeps is encapsulated in headCount(), which is almost always called in the roleCall phase. In the Screeps official tutorial code, you found your creeps by filtering Game.creeps. When the length of the array returned was less than your desired count, it spawned another one. Our headCount() basically does the same thing, but in a more CPU-friendly way. If you'd like a look under the hood, you can find the function here.

Here is what we are about to write:

import {Mission} from "./Mission";
import {Operation} from "./Operation";
export class HarvestMission extends Mission {

    // define your role-array
    harvesters: Creep[];

    constructor(operation: Operation) {
        super(operation, "harvest");
    }

    initMission() {
    }

    roleCall() {
        // find and spawn creeps for your role
        this.harvesters = this.headCount("harvester", () => this.workerBody(1, 1, 1), 1);
    }

    missionActions() {
    }

    finalizeMission() {
    }

    invalidateMissionCache() {
    }
}

Lets start out with spawning some creeps to harvest our energy. First we want to create the class member that will hold the array, which we will call harvesters and make it of type Creep[].

Next we use headCount to find/spawn our creeps in the roleCall function. Because we are using typescript, the function signature can be easily displayed in your IDE, but lets look at it here too:

protected headCount(roleName: string, getBody: () => string[], max: number, options?: HeadCountOptions): Creep[] 

This signature tells us that we want to start by passing in the name of our role, the body, and the number we need. One thing that might be unexpected is the use of the arrow function with the getBody variable. This allows us to only run the code necessary to build the body when we actually need it (when we are spawning a new creep). This saves us just a little bit of CPU per mission, which really adds up when you start to have hundreds of missions running at the same time.

Although they are beyond the scope of this tutorial, here is some of the other features of headCount:

  • If you have more than one spawn in the room, headCount will automatically make use of every spawn available.
  • Define which boosts your creeps will need (HeadCountOptions)
  • Pre-spawn your creeps so that they are ready to go when a currently-spawned creep times out
  • Travel waypoints before getting to work

Because we are using typescript, we don't have to remember all the arguments required and what order they go in, just let your IDE tell you what goes next:

typescript features in the IDE

Adding creep behavior in missionActions()

Let's make our harvesters find a source and start harvesting. We can put this behavior in the missionActions function. Instead of throwing all the code directly in the function block, I usually stick with the convention of defining a separate roleActions(creep) function for every role that I am using. Here is what it will look like:

missionActions() {
    for (let harvester of this.harvesters) { this.harvesterActions(harvester) }
}

private harvesterActions(harvester: Creep) {
    let source = this.room.find<Source>(FIND_SOURCES)[0];
    if (harvester.pos.isNearTo(source)) {
        harvester.harvest(source);
    }
    else {
        harvester.blindMoveTo(source);
    }
}

This code will have the harvester look for the first source returned by room.find(). If it is nearby, harvest. If not, move to the source. We use the function creep.blindMoveTo(target) which acts as a wrapper for moveTo. It is "blind" because it doesn't account for other creeps in pathfinding unless the creep gets stuck. You can take look at what is happening under the hood if you are curious.

In the second part of this tutorial we will have it utilize as many sources as there are in the room, and we will also spawn creeps to carry the energy away. Let's keep it simple for now and finish off part 1 by showing how to define our operation and take a look at what our code is doing so far in the simulation.

Defining your first operation: BaseOperation

We can create our operation class and generate the required members to provide us with a template, this same way we did with mission. Here is what we are about to write:

import {Operation} from "./Operation";
import {Empire} from "./Empire";
import {OperationPriority} from "./constants";
import {HarvestMission} from "./HarvestMission";
export class BaseOperation extends Operation {

    // create our constructor (this is boilerplate and will be nearly the same for every operation)
    constructor(flag: Flag, name: string, type: string, empire: Empire) {
        super(flag, name, type, empire);
        this.priority = OperationPriority.OwnedRoom
    }

    initOperation() {
        // this line tells our operation where to spawn from
        this.spawnGroup = this.empire.getSpawnGroup(this.flag.room.name);
        // instantiate our mission
        this.addMission(new HarvestMission(this))
    }

    finalizeOperation() {
    }

    invalidateOperationCache() {
    }

}

Whereas Mission had all four phases, Operation only participates in two of those phases, at least at the concrete level. Under the hood, it is also getting called during the roleCall and actions phases but only to execute the mission functions for every mission that it holds.

The first line in initOperation() tells our creeps where to spawn from. Each operation holds a reference to an instance of Empire, which does all the organization for our various rooms. It knows which rooms we can spawn from. In this case, the obvious choice is to spawn from the room that holds the operation flag (more on this flag below).

As you can see on the next line, instantiating our mission is as simple as using the new keyword and passing in the operation as an argument. The value returned is passed as an argument to addMission(mission). If we had more than one mission, the creeps for each would get spawned in the order they are added.

Adding your operation to loopHelper.ts

In order to instantiate our operation in the screeps world, we need to add its type to a special function that gets called from main.ts. Open up src/loopHelper.ts in your IDE/editor and find the OPERATION_CLASSES constant declared near the top, which is a hash object that takes the type as the hash and returns the class as a value. Let's have our operation be of type "base". We simply need to add the line base: BaseOperation,. After we are done, our constant in src/loopHelper.ts will look something like this:

const OPERATION_CLASSES = {
    conquest: ConquestOperation,
    fort: FortOperation,
    mining: MiningOperation,
    tran: TransportOperation,
    keeper: KeeperOperation,
    demolish: DemolishOperation,
    raid: RaidOperation,
    quad: QuadOperation,
    auto: AutoOperation,
    flex: FlexOperation,
    base: BaseOperation, // we just added this line
};

This constant gets used in loopHelper.getOperations() which gets called from module.exports.loop in main.ts. Make sure your IDE has added the required import. If not, add the following line to the top of src/loopHelper.ts:

import {BaseOperation} from "./BaseOperation";

Instantiating with a flag

Operations are currently instantiated by placing a flag with a special name. The flag name determines the type of operation and the operation name. The convention used is type_name. So if we want to name our operation "foobar1", we will use the flagname base_foobar1 to create and run an instance of our operation in the screep world. Let's take a look at what this looks like in the simulation.

Here are the steps taken in the following gif:

  1. Add a couple sources
  2. Add a spawn
  3. Add the flag base_foobar1

Instantiating foobar1

Conclusion

That's it! Now your code is directly linked to the game loop, and will get instantiated anytime/anywhere you want just by placing a flag. It will instantiate a new operation for each flag that matches the pattern base_opName. So if you want to control another room, you could place another flag: base_foobar2.

Look at the OPERATION_CLASSES variable we modified above. Each of those corresponds to a different operation that I've already written, and all of these are instantiated the exact same way. For example, if you want to remote harvest from a room, you can simply drop the flag mining_remote1 in the room. It will find the nearest room to spawn from and deliver to (that room would require a spawn and a storage) and begin spawning creeps to do the work. All the necessary road/container construction is bootstrapped from simply placing that flag.

In the next part we will make our mission more useful by carrying the energy back to the spawn. We will also spawn creeps to harvest each source in the room.

Here are the finished files used in this tutorial, for your reference: