Femboy.nu

Screeps

I recently got into a programming game called screeps, which is in its own words “An open source MMO strategy sandbox game. You control your colony by writing JavaScript that operates 24/7 in the huge persistent online open world.”, but instead of using the public server, I started playing on a private server managed by a friend.

A screenshot

How does it work?

The game feels very similar to a game I had as a kid. I do know know exactly which game that it was, but it looked very similar to one of the many Karel clients. In Screeps, you are a god in a world, where and you are given a ‘spawner’. This spawner can spawn creeps, and those creeps can do all sorts of actions. But they will not do any of that without you telling them exactly what to do. You tell them what to do using javascript (or another language that can target webassembly). After joining the game and placing down your spawner, you can spawn a creep using:

Game.rooms['sim'].find(FIND_MY_SPAWNS)[0].spawnCreep(['work', 'carry', 'move'], 'my-creep')

and this creep will have the name “my-creep” and three bodyparts (‘work’, ‘carry’ and ‘move’). You could for example remove the ‘move’ bodypart, and your creep would be unable to move itself (other creeps that have the ability to do so can still move it). If you want the creep to be able to carry more, you might want to add multiple ‘carry’ bodyparts. This ofcourse makes the creep more expensive to spawn.

Energy

Energy is the most important resource in the game, many important actions cost energy. spawning creeps cost energy, building structures costs energy and upgrading your room also costs energy. Because so many actions cost energy, you will want energy, a lot of energy.

Energy can be harvested from ‘sources’, that exist in the world. These sources regenerate over time. To harvest them, you will need to create a creep that has the ‘work’ bodypart, move it next to the source and then harvest the source. This will result in a few energy being generated per game-tick.

The following code will move ever creep to the first source found in a room,and attempt to harvest it. Ofcourse the energy that is being harvested then needs to be moved into the spawner, or is used while creating a structure.

for(let creep of Object.values(Game.creeps)) {
    let source = creep.room.find(FIND_SOURCES)[0]
    let distance = creep.pos.getRangeTo(source.pos);
    
    if(distance > 1) {
        creep.moveTo(source.pos)
    } else {
        creep.harvest(source);
    }
}

Architecture

When you know the basics of the game, other questions should come to your mind. Such as “What decides what each creep does”, “what decides that creeps should exist?” or other global architecture questions. And the beauty of the game is, that you can decide this. Want to create a giant decision tree? go ahead, want to hardcode a set of creeps that all follow a set routine? go ahead.

I decided to create a task-based system. where all actions that need to happen can be described as a task. A task may be “Harvest this source” or “Move energy from spot X into the spawner”. A small self-contained unit of work that a creep can execute. But not all creeps are made equal. If a task decides that a source must be harvested, then the creep with the best harvesting ability should execute it. That is why all of my tasks have a piece of metadata attached, that describe the ideal creep, and a minimum-spec creep. Then the creeps can be sorted based on ability before being assigned a task. And if a task is unassigned for too long, a creep can be spawned to fill the vacancy. But before sorting creeps, you must not forget to sort the tasks based on importance first.

After a few days of playing these are the tasks that exist in my design, together with the main task meta-data

export enum TaskType {
  FRESH_SPAWN = "Fresh Spawn",
  HARVEST_SOURCE = "Harvest Source",
  HARVEST_EXTERNAL_SOURCE = "Harvest External Source",
  HAUL_EXTERNAL_ROOM = "Haul External Room",
  UPGRADE_CONTROLLER = "Upgrade Controller",
  FEED_SPAWNER = "Feed Spawner",
  FEED_STORAGE = "Feed Storage",
  CONSTRUCT = "Construct",
  REPAIR = "Repair",
  COLONIZE = "Colonize",
  DEFEND = "Defend",
  EXPLORE = "Explore",
}

export type AbilitySpecification = { [part in BodyPartConstant]?: number };

export type BaseTask = {
  sequence: number;
  createdAt: number;
  desiredAbilities: AbilitySpecification;
  requiredAbilities: AbilitySpecification;
  assignedCreep?: string;
};

Tasks can then be created, and executed. Here is an example for my harvest source task (one of the most simple of all tasks)

export function ensureHarvestSourceTasks(room: Room) {
  if (Game.time % 10 != 0) {
    // Only attempt to create harvest tasks once every 10 ticks
    // This limits wasted cpu usage.
    return;
  }

  const actualTasks = room.memory.taskQueue.filter((x) => x.type == TaskType.HARVEST_SOURCE);
  const requiredSources = room.find(FIND_SOURCES);

  for (const source of requiredSources) {
    if (actualTasks.find((x) => source.pos.isEqualTo(toRoomPosition(x.meta.startPosition)))) {
      // Only create a new task if there is no existing task for this source
      continue;
    }

    const containerPos = getNearbyContainer(room, source.pos, 1);
    const container = containerPos
      .lookFor(LOOK_STRUCTURES)
      .filter((x) => x.structureType == STRUCTURE_CONTAINER)
      .shift() as StructureContainer | undefined;
    if (container && container.store.getFreeCapacity(RESOURCE_ENERGY) == 0) {
      // If the storage location next to this source is full, harvesting it is pointless.
      continue;
    }

    room.memory.taskQueue.push({
      type: TaskType.HARVEST_SOURCE,
      createdAt: Game.time,
      requiredAbilities: { [MOVE]: 1, [WORK]: 2 },
      desiredAbilities: { [MOVE]: 1, [WORK]: 6 },
      sequence: Sequence.get("task"),
      meta: {
        startPosition: source.pos,
        targetPosition: new RoomPosition(containerPos.x, containerPos.y, room.name),
      },
    });
    return;
  }
}

export function executeHarvestSource(creep: Creep) {
  const meta = creep.memory.currentTask?.meta as HarvestSourceMeta;

  // Go to the target position and harvest the source
  creep.memory.currentStep = "gather";
  const targetPosition = toRoomPosition(meta.targetPosition);
  const sourcePosition = toRoomPosition(meta.startPosition);
  const distanceToTarget = creep.pos.getRangeTo(targetPosition);
  if (distanceToTarget > 0) {
    creep.moveTo(targetPosition, { visualizePathStyle: { stroke: "#ffffffff", opacity: 0.9 } });
  } else {
    creep.harvest(sourcePosition.lookFor(LOOK_SOURCES).shift()!);
  }
}

Other players

Because its an MMO, other players also exist in the world. And their script run 24/7, just like yours. Can attempt to attack them to steal their resources, or maybe execute other sneaky actions. The server owner can also decide to place bots into the world that act just like other players (a script). These players can also attempt to grief you, or even send catgirl creeps your way (which are executed on sight by the guards, deserved).

Another player's creep that makes cat noises

Conclusion

This game has eaten many of my hours in the past few weeks, its really fun and well thought out. It continues to offer challenges of just the right difficulty to keep you hooked. There is also plenty of content, and multiple features that I have not even touched yet. If you’re interested, I highly recommend that you give the game a try. Its frequently on sale, on steam. This is what my main room looks like right now:

A screenshot
Thank you for reading this article.
If you spot any mistakes or if you would like to contact me, visit the contact page for more details.