Behavior State Machines This document describes the state machine architecture used for creep behavior management in the Screeps bot runtime.
Overview The bot uses state machines to model and execute creep behaviors. Each role (harvester, upgrader, builder, etc.) is implemented as a dedicated state machine that:
Defines explicit states (idle, harvesting, delivering, etc.)
Declares valid state transitions via events
Manages role-specific context and behavior
Persists state across tick boundaries
Integrates with the kernel process architecture
This approach replaced the monolithic BehaviorController pattern to improve modularity, testability, and maintainability.
Architecture Components 1. State Machines Location : packages/bot/src/runtime/behavior/stateMachines/
Each role has a dedicated state machine file that defines:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 export interface HarvesterContext { creep : Creep ; sourceId ?: Id <Source >; targetId ?: Id <AnyStoreStructure >; } export type HarvesterEvent = | { type : "START_HARVEST" ; sourceId : Id <Source > } | { type : "ENERGY_FULL" } | { type : "START_DELIVER" ; targetId : Id <AnyStoreStructure > } | { type : "ENERGY_EMPTY" }; export const harvesterStates : Record <string , StateConfig <HarvesterContext , HarvesterEvent >> = { idle : { onEntry : [ctx => { ctx.sourceId = undefined ; ctx.targetId = undefined ; }], on : { START_HARVEST : { target : "harvesting" , actions : [(ctx, event ) => { if (event.type === "START_HARVEST" ) { ctx.sourceId = event.sourceId ; } }] } } }, harvesting : { on : { ENERGY_FULL : { target : "delivering" }, SOURCE_DEPLETED : { target : "idle" } } }, delivering : { on : { ENERGY_EMPTY : { target : "idle" }, TARGET_FULL : { target : "idle" } } } }; export const HARVESTER_INITIAL_STATE = "idle" ;
Key Features :
Type-Safe Context : Each role defines its own context interface
Event-Driven : State transitions triggered by typed events
Guards : Conditional transitions based on context state
Actions : Side effects executed during transitions or state entry/exit
Serializable : State machines can be serialized to/from memory
2. Role Controllers Location : packages/bot/src/runtime/behavior/controllers/
Each role has a controller class that:
Implements the RoleController interface
Manages the state machine lifecycle for creeps
Executes state-specific behavior logic
Handles memory validation and migration
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 import { profile } from "@ralphschuler/screeps-profiler" ;import type { RoleController , RoleConfig } from "./RoleController" ;@profile export class HarvesterController implements RoleController <CreepMemory > { getRoleName (): string { return "harvester" ; } getConfig (): RoleConfig <CreepMemory > { return { minimum : 2 , body : [WORK , CARRY , MOVE ], createMemory : () => ({ role : "harvester" }), version : 1 }; } createMemory (): CreepMemory { return { role : "harvester" }; } validateMemory (creep : CreepLike ): void { if (!creep.memory .role ) { creep.memory .role = "harvester" ; } } execute (creep : CreepLike ): string { const machine = stateMachineManager.getMachine (creep.name ); if (!machine) return "idle" ; const currentState = machine.getCurrentState (); switch (currentState) { case "idle" : return this .executeIdle (creep, machine); case "harvesting" : return this .executeHarvesting (creep, machine); case "delivering" : return this .executeDelivering (creep, machine); default : return "idle" ; } } private executeHarvesting (creep : CreepLike , machine : StateMachine ): string { const ctx = machine.getContext (); if (!ctx.sourceId ) { machine.send ({ type : "SOURCE_DEPLETED" }); return "idle" ; } const source = Game .getObjectById (ctx.sourceId ); if (!source) { machine.send ({ type : "SOURCE_DEPLETED" }); return "idle" ; } const result = (creep as Creep ).harvest (source); if (result === OK && creep.store .getFreeCapacity (RESOURCE_ENERGY ) === 0 ) { machine.send ({ type : "ENERGY_FULL" }); } return "harvest" ; } }
3. RoleControllerManager Location : packages/bot/src/runtime/behavior/RoleControllerManager.ts
The manager orchestrates all role controllers:
Registers all role controllers at initialization
Coordinates spawning based on role minimums
Executes creep behavior via appropriate controller
Manages CPU budget to prevent timeouts
Integrates with kernel as a process
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import { process } from "@ralphschuler/screeps-kernel" ;import { profile } from "@ralphschuler/screeps-profiler" ;@process ({ name : "RoleControllerManager" , priority : 50 , singleton : true })@profile export class RoleControllerManager { private readonly roleControllers : Map <string , RoleController >; constructor ( ) { this .roleControllers = new Map (); this .registerRoleController (new HarvesterController ()); this .registerRoleController (new UpgraderController ()); this .registerRoleController (new BuilderController ()); } public execute (game : GameContext , memory : Memory ): BehaviorSummary { this .ensureRoleMinimums (game, memory, roleCounts); for (const creep of Object .values (game.creeps )) { const controller = this .roleControllers .get (creep.memory .role ); if (controller) { controller.execute (creep); } } return summary; } }
4. StateMachineManager Location : packages/bot/src/runtime/behavior/StateMachineManager.ts
Manages state machine instances:
Initializes state machines for all creeps
Restores machines from memory across ticks
Persists machine state to memory
Cleans up machines for dead creeps
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 export class StateMachineManager { private machines : Map <string , StateMachine <CreepContext , CreepEvent >>; initialize (creeps : { [name : string ]: Creep }): void { for (const name in creeps) { const creep = creeps[name]; const role = creep.memory .role ; const config = ROLE_CONFIGS [role]; if (creep.memory .stateMachine ) { const machine = restore (creep.memory .stateMachine , config.states ); machine.getContext ().creep = creep; this .machines .set (name, machine); } else { const context = this .createInitialContext (creep, role); const machine = new StateMachine (config.initialState , config.states , context); this .machines .set (name, machine); } } } persist (creeps : { [name : string ]: Creep }): void { for (const [name, machine] of this .machines ) { const creep = creeps[name]; if (creep) { creep.memory .stateMachine = serialize (machine); } } } }
State Machine Patterns Basic State Transitions 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 export const upgraderStates = { recharge : { on : { ENERGY_FULL : { target : "upgrading" , guard : ctx => ctx.creep .store .getFreeCapacity (RESOURCE_ENERGY ) === 0 } } }, upgrading : { on : { ENERGY_EMPTY : { target : "recharge" , guard : ctx => ctx.creep .store .getUsedCapacity (RESOURCE_ENERGY ) === 0 } } } };
Conditional Transitions with Guards 1 2 3 4 5 6 7 8 9 10 11 12 { on : { CHECK_STORAGE : { target : "delivering" , guard : ctx => { const storage = ctx.creep .room .storage ; return storage && storage.store .getFreeCapacity (RESOURCE_ENERGY ) > 1000 ; } } } }
State Entry/Exit Actions 1 2 3 4 5 6 7 8 9 10 11 12 13 14 { idle : { onEntry : [ctx => { ctx.sourceId = undefined ; ctx.targetId = undefined ; }], onExit : [ctx => { console .log (`${ctx.creep.name} leaving idle state` ); }] } }
Transition Actions 1 2 3 4 5 6 7 8 9 10 11 12 13 14 { on : { START_HARVEST : { target : "harvesting" , actions : [(ctx, event ) => { if (event.type === "START_HARVEST" ) { ctx.sourceId = event.sourceId ; ctx.creep .say ("⛏️" ); } }] } } }
Role-Specific State Machines Harvester (Energy Collection + Delivery) States : idle, harvesting, delivering
Flow :
idle → Find source → START_HARVEST → harvesting
harvesting → Energy full → ENERGY_FULL → delivering
delivering → Energy empty → ENERGY_EMPTY → idle
Context : { creep, sourceId, targetId }
Upgrader (Recharge + Upgrade) States : recharge, upgrading
Flow :
recharge → Collect energy → ENERGY_FULL → upgrading
upgrading → Use energy → ENERGY_EMPTY → recharge
Context : { creep, sourceId }
Builder (Recharge + Build/Repair) States : recharge, building, repairing
Flow :
recharge → Collect energy → ENERGY_FULL → building or repairing
building → Build complete or energy empty → recharge or repairing
repairing → Repair complete or energy empty → recharge
Context : { creep, sourceId, targetId, taskType }
Hauler (Pickup + Deliver) States : idle, collecting, delivering
Flow :
idle → Find pickup → START_COLLECT → collecting
collecting → Energy full → ENERGY_FULL → delivering
delivering → Energy empty → ENERGY_EMPTY → idle
Context : { creep, pickupId, deliveryId }
Remote Roles (Travel + Work) States : travelToTarget, working, travelToHome, deposit
Flow :
travelToTarget → Arrive → ARRIVED → working
working → Full or task complete → FULL or TASK_COMPLETE → travelToHome
travelToHome → Arrive → ARRIVED → deposit
deposit → Empty → EMPTY → travelToTarget
Context : { creep, homeRoom, targetRoom, ... }
Integration with Kernel State machines integrate with the kernel process architecture:
1 2 3 4 5 6 7 8 9 import { Kernel } from "@ralphschuler/screeps-kernel" ;import "./runtime/behavior/RoleControllerManager" ; const kernel = new Kernel ({ logger : console });export const loop = ( ) => { kernel.run (Game , Memory ); };
The RoleControllerManager is registered as a kernel process with:
Priority : 50 (core gameplay)
Singleton : true (single instance reused across ticks)
Execution : Automatic via kernel scheduling
Memory Persistence State machines serialize to memory for cross-tick persistence:
1 2 3 4 5 6 7 8 9 10 { "stateMachine" : { "currentState" : "harvesting" , "context" : { "sourceId" : "5bbcab9c9099fc012e638441" , "targetId" : "5bbcab9c9099fc012e638442" } } }
Serialization Process :
StateMachineManager.persist() called at end of tick
Each machine serialized via serialize(machine)
Stored in creep.memory.stateMachine
On next tick, restored via restore(creep.memory.stateMachine, states)
Creep reference updated (Game object changes each tick)
Testing State Machines Unit Testing 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import { describe, it, expect } from "vitest" ;import { StateMachine } from "@ralphschuler/screeps-xstate" ;import { harvesterStates, HARVESTER_INITIAL_STATE } from "./harvester" ;describe ("Harvester State Machine" , () => { it ("should transition from idle to harvesting" , () => { const context = { creep : mockCreep () }; const machine = new StateMachine ( HARVESTER_INITIAL_STATE , harvesterStates, context ); expect (machine.getCurrentState ()).toBe ("idle" ); machine.send ({ type : "START_HARVEST" , sourceId : "source123" }); expect (machine.getCurrentState ()).toBe ("harvesting" ); expect (machine.getContext ().sourceId ).toBe ("source123" ); }); it ("should transition to delivering when energy full" , () => { const context = { creep : mockCreep ({ store : { energy : 50 } }) }; const machine = new StateMachine ("harvesting" , harvesterStates, context); machine.send ({ type : "ENERGY_FULL" }); expect (machine.getCurrentState ()).toBe ("delivering" ); }); });
Integration Testing 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 describe ("HarvesterController Integration" , () => { it ("should execute harvest and transition states" , () => { const controller = new HarvesterController (); const creep = createMockCreep ({ role : "harvester" }); stateMachineManager.initialize ({ [creep.name ]: creep }); const task1 = controller.execute (creep); expect (task1).toBe ("harvest" ); creep.store [RESOURCE_ENERGY ] = creep.store .getCapacity (); const task2 = controller.execute (creep); expect (task2).toBe ("deliver" ); }); });
Best Practices 1. Keep States Focused Each state should have a clear, single responsibility:
1 2 3 4 5 6 7 8 9 10 11 12 13 harvesting : { on : { ENERGY_FULL : { target : "delivering" } } } working : { on : { ENERGY_FULL : { target : "delivering" }, FOUND_CONSTRUCTION : { target : "building" }, FOUND_REPAIR : { target : "repairing" } } }
2. Use Guards for Validation Add guards to prevent invalid transitions:
1 2 3 4 5 6 7 on : { START_UPGRADE : { target : "upgrading" , guard : ctx => ctx.creep .room .controller ?.my === true } }
3. Minimize Context Size Keep context lean to reduce memory overhead:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 interface HarvesterContext { creep : Creep ; sourceId ?: Id <Source >; targetId ?: Id <Structure >; } interface HarvesterContext { creep : Creep ; source ?: Source ; target ?: Structure ; pathCache ?: PathFinderPath ; }
4. Handle Invalid States Always have fallback behavior:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 execute (creep : CreepLike ): string { const machine = stateMachineManager.getMachine (creep.name ); if (!machine) { return "idle" ; } const state = machine.getCurrentState (); if (!this .stateHandlers [state]) { machine.send ({ type : "RESET" }); return "idle" ; } return this .stateHandlers [state](creep, machine); }
5. Use Descriptive Event Names Events should clearly describe what happened:
1 2 3 4 5 6 7 8 9 10 11 type HarvesterEvent = | { type : "SOURCE_DEPLETED" } | { type : "TARGET_FULL" } | { type : "ENERGY_EMPTY" }; type HarvesterEvent = | { type : "DONE" } | { type : "ERROR" } | { type : "NEXT" };
Memory Overhead
Each serialized state machine: ~50-100 bytes
Simple states (idle/recharge): minimal overhead
Complex states with IDs: ~100-150 bytes
CPU Overhead
State machine lookup: ~0.01 CPU
State transition: ~0.02 CPU
Serialization: ~0.03 CPU per machine
Total per creep: ~0.06-0.10 CPU
Optimization Tips
Use Singleton RoleControllerManager : Reuse instance across ticks
Batch State Machine Operations : Initialize/persist all at once
Lazy Context Updates : Only update context when needed
Cache Controller Lookups : Store controller references
Migration from BehaviorController If migrating from the old BehaviorController pattern:
Identify Role Behaviors : Extract each role’s logic from monolithic controller
Define States : Map behavior phases to explicit states
Define Events : Identify triggers for state transitions
Create State Machine : Implement state definitions
Create Controller : Implement RoleController interface
Register Controller : Add to RoleControllerManager
Test : Unit test state machine and controller
Deploy : Verify in-game behavior matches old system
Remove Old Code : Delete obsolete BehaviorController code
See Behavior Migration Guide for detailed steps.
Examples Repository Full state machine implementations available at:
packages/bot/src/runtime/behavior/stateMachines/ - All role state machines
packages/bot/src/runtime/behavior/controllers/ - All role controllers
packages/bot/tests/unit/behavior/ - Unit tests
packages/bot/tests/e2e/behavior/ - Integration tests