Screeps Tutorial – Building on the Creep State Machine

0
1451

In our last tutorial we explored a state machine design for running a creep role. In this tutorial, we will build on this further so that we can handle more diverse situations but while still reusing as much code as possible.

Lets revisit the run function. This is for a ‘hauler’ type creep that will collect a resource from one spot and transport it to another.


const STATE_SPAWNING = 0;
const STATE_MOVING = 1;
const STATE_HARVESTING = 2;
const STATE_DEPOSIT_RESOURCE = 3;
const STATE_GRAB_RESOURCE = 4;
...
var run = function(creep) {
if(!creep.memory.state) {
creep.memory.state = STATE_SPAWNING;
}
switch(creep.memory.state) {
case STATE_SPAWNING:
runSpawning(creep, STATE_MOVING);
break;
case STATE_MOVING:
runMoving(creep, STATE_GRAB_RESOURCE);
break;
case STATE_GRAB_RESOURCE:
runGrabResource(creep, STATE_MOVING);
break;
case STATE_DEPOSIT_RESOURCE:
runDepositResource(creep, STATE_MOVING);
break;
}
};


Depending how closely you looked at that, you may have noticed 2 problems:

  • How will the creep know where to move?
  • When the creep is in STATE_MOVING, it always transitions to STATE_GRAB_RESOURCE and never to STATE_DEPOSIT_RESOURCE.
  •  
    This is a limitation with the current design strategy. If we wanted to stick with this style, then we would need to separate STATE_MOVING into 2 states for the hauler. Perhaps something like STATE_MOVE_TO_GRAB and STATE_MOVE_TO_DEPOSIT. We could set up these locations in memory when the creep is initialized in STATE_SPAWNING... but the code for the movement states is going to be almost identical. Similarly, there may be other creep roles that you would like to reuse the movement code for... If we continue with this I'm sure we'll end up with multiple versions of movement code for various creeps. That's going to be a pain to maintain.

    The problem is that when we go into STATE_MOVING we also need to give it some context. This context will affect not only the target position to move to, but it also determines the next state to transition to (STATE_GRAB_RESOURCE or STATE_DEPOSIT_RESOURCE). You can solve this problem a few different ways, and what I propose below is just my take on it.

    
    var run = function(creep) {
    if(!creep.memory.state) {
    creep.memory.state = STATE_SPAWNING;
    }
    switch(creep.memory.state) {
    case STATE_SPAWNING:
    runSpawning(creep, {nextState: STATE_MOVING});
    break;
    case STATE_MOVING:
    runMoving(creep, {context: haulerContext});
    break;
    case STATE_GRAB_RESOURCE:
    runGrabResource(creep, {nextState: STATE_MOVING});
    break;
    case STATE_DEPOSIT_RESOURCE:
    runDepositResource(creep, {nextState: STATE_MOVING});
    break;
    }
    };
    var haulerContext = function(creep, currentState) {
    switch(currentState) {
    case STATE_MOVING:
    if(_.sum(creep.carry) > 0) {
    creep.memory.targetPos = getHaulerDepositTarget(creep);
    return {nextState: STATE_DEPOSIT_RESOURCE};
    } else {
    creep.memory.targetPos = creep.memory.sourcePos;	// or perhaps you're very fancy and you have a function that dynamically assigns your haulers...
    return {nextState: STATE_GRAB_RESOURCE};
    }
    break;
    ...
    }
    };
    var getHaulerDepositTarget = function(creep) {
    // We work out where to put the resources...
    // Perhaps we fill the spawn/extensions...
    // Perhaps we deposit into the storage/terminal...
    // Perhaps we fill towers, labs, nukers, power spawn, etc...
    // It depends on your code!
    };
    var runMoving = function(creep, options) {
    var transitionState = options.context ? haulerContext(creep, STATE_MOVING).nextState : options.nextState;
    // We know that creep.memory.targetPos is set up before this state is called. For haulers, it's set in haulerContext(), for other creep roles it would be set somewhere else...
    var pos = new RoomPosition(creep.memory.targetPos.x, creep.memory.targetPos.y, creep.memory.targetPos.roomName);
    // Has the creep arrived?
    if(creep.pos == pos) {
    creep.memory.state = transitionState;
    run(creep);
    return;
    }
    // It hasn't arrived, so we get it to move to targetPos
    creep.moveTo(pos);
    };
    

    It may not be obvious what we've done above, so I'll run through it.

    In the run function you will see that instead of passing in the next state to all of the run functions, we're now passing in an options object. This object can have a context property or a nextState property. When you think about this object, just treat it as the "it knows which state to go into next" object. Either it has the nextState property set, or sets the context property with the function that will figure out the next state (along with setting up some things in the creep's memory).

    In the haulerContext function, you can see that this is were we figure out if the creep should pick up resources or deposit them. It does this by seeing if the creep is empty or not. The creep.memory.targetPos is also set here. The targetPos is used by:

  • STATE_MOVING to know where to move to,
  • STATE_GRAB_RESOURCE (not shown) to know where to grab the resource from,
  • STATE_DEPOSIT_RESOURCE (not shown) to know where to put the resource.
  •  
    In the runMoving function, we set the transitionState using the options object (remember, its the "it knows which state to go into next" object). If the context property is set, we use that to get the nextState. Apart from that, nothing else has changed in the runMoving function from the previous tutorial. If that line of code is confusing, the code below would also do the same thing:

    
    var transitionState = options.context ? haulerContext(creep, STATE_MOVING).nextState : options.nextState;
    // The line above does the same as the  code below...
    var transitionState;
    if(options.context) {
    transitionState = haulerContext(creep, STATE_MOVING).nextState;
    } else {
    transitionState = options.nextState;
    }
    

    And there we have it. The runMoving function can now be reused by creeps across different roles, and even by the same role but with different a targetPos based on the current context (does the creep already have resources...). You could apply this strategy to any of the states. For example, when in STATE_GRAB_RESOURCE you might want to .look() at the targetPos to see if there are resources on the ground to pick up, or if there is a container/storage/terminal/... to withdraw the resources from, or if there is a creep to transfer resources from. Similarly, you might want the hauler to deposit resources into the storage unless the storage is full, or maybe you prioritise filling the towers if the room is under attack, or perhaps in another circumstance they should try to transfer energy to another creep that is repairing a wall... We now have a place to define all of this custom logic that will alter the creep's behaviour. It may continue to run the same cycle in the state machine, or it might even follow a different flow through the state machine. In fact, it could even reassign it's own role to do something else entirely.

    What did you think about this tutorial? Should I continue further on this topic? or perhaps theres something else you guys would like to look at. Feel free to comment on this article, or get in touch with me on Slack for your suggestions and feedback.