Quickstart: creating your first robot

Any robot program is really just a function that accepts the state of the game and returns the robot’s action. Let’s take another look at the default robot:

Python

def robot(state, unit):
    if state.turn % 2 == 0:
        return Action.move(Direction.East)
    else:
        return Action.attack(Direction.South)

Javascript

function robot(state, unit) {
  if (state.turn % 2 === 0) {
    return Action.move(Direction.East)
  } else {
    return Action.attack(Direction.South)
  }
}

The state of the arena is passed as the first argument, and information about this specific unit is passed as the second.

The body of this function must decide this robot’s specific move. Recall that you have three options: move, attack, or pass (by returning None/null). In this case, the robot alternates between moving east and attacking south.

During a battle simulation, every turn, this function is run for every one of your robots. After the same is done for the opponent’s robots, the RR logic core resolves any movement conflicts, updates the arena state, and the cycle begins over again. The two programs are run in sandboxed processes that maintain execution context over the course of the entire battle. This means that you are free to create global variables and use them in your functions as desired!

Reading the state

Before any robot is run, its code is combined with one of our standard libraries (more info in The complete API reference). These stdlib’s provide several useful classes for dealing with the game state:

class State

This is the root class for all game state. Go here for arena-wide information, like:

turn
our_team
other_team
objs_by_team(team)
obj_by_coords(coords)

These last two methods are particularly useful, because they return information about the units, contained in the Obj class:

class Obj

Anything that can exist within a grid tile is considered an Obj. Right now, there are two types: ObjType.Unit and ObjType.Terrain.

id
coords
obj_type
team
health

The stdlib also provides a special class for working with coordinates:

class Coords
x, y
distance_to(other)
direction_to(other)

…and several useful enums, like Direction.

Armed with these new tools, we can drastically improve our robot program. Let’s use the Coords.distance_to() method to find the closest enemy to the current robot and attack them:

Python

def robot(state, unit):
    enemies = state.objs_by_team(state.other_team)
    closest_enemy = min(enemies,
        key=lambda e: e.coords.distance_to(unit.coords)
    )
    direction = unit.coords.direction_to(closest_enemy.coords)

    if unit.coords.distance_to(closest_enemy.coords) == 1:
        # we're right next to them
        return Action.attack(direction)
    else:
        return Action.move(direction)

Javascript

function robot(state, unit) {
  enemies = state.objsByTeam(state.otherTeam)
  // the `_` here is from lodash (https://lodash.com/), a set of nice js functions
  // it's included by default in RR
  closestEnemy = _.minBy(enemies,
    e => e.coords.distanceTo(unit.coords)
  )
  direction = unit.coords.directionTo(closestEnemy.coords)

  if (unit.coords.distanceTo(closestEnemy.coords) === 1) {
    // we're right next to them
    return Action.attack(direction)
  } else {
    return Action.move(direction)
  }
}

Coordinating your army

If you’ve ever played Starcraft, you’re probably familiar with the terms micro and macro. Micro refers to the local decisions of your units, like how to maneuver between enemies and deliver a well-timed attack. Macro, on the other hand, refers to your high-level strategy, like when and where to move your armies.

Just like in Starcraft, any good RR robot needs a combination of micro and macro to win. As you just saw, coding micro is relatively straightforward, since it comes down to simple logical decisions. But macro is much more difficult — it involves long-term planning, creativity, adaptability! And no, you can’t cheat by importing a neural network and letting it do the work for you. Coding macro is really like coding anything else: you just need good abstraction, and a hell of a lot of patience.

A good place to start is with implementing coordination. Although the robot() function runs individually for every robot, you can use the global scope to share information and strategize. Let’s improve our program by asking all of our robots to focus on one target (and we’ll use Debug.locate() to highlight that target in the map):

Python

target_id = None

def robot(state, unit):
    global target_id

    if target_id:
        if not state.obj_by_id(target_id):
            # target has been defeated
            target_id = None

    if not target_id:
        allies = state.objs_by_team(state.our_team)

        def total_distance_for_team(enemy):
            return sum([ally.coords.distance_to(enemy.coords) for ally in allies])

        enemies = state.objs_by_team(state.other_team)
        closest_enemy_for_team = min(enemies,
            key=total_distance_for_team
        )
        target_id = closest_enemy_for_team.id

    target = state.obj_by_id(target_id)
    debug.locate(target)
    direction = unit.coords.direction_to(target.coords)

    if unit.coords.distance_to(target.coords) == 1:
        # we're right next to them
        return Action.attack(direction)
    else:
        return Action.move(direction)

Javascript

let targetId = null

function robot(state, unit) {
  if (targetId) {
    if (!state.objById(targetId)) {
      // target has been defeated
      targetId = null
    }
  }

  if (!targetId) {
    allies = state.objsByTeam(state.ourTeam)

    const totalDistanceForTeam = (enemy) =>
      _.sum(allies.map(ally => ally.coords.distanceTo(enemy.coords)))

    enemies = state.objsByTeam(state.otherTeam)
    closestEnemyForTeam = _.minBy(enemies, totalDistanceForTeam)
    targetId = closestEnemyForTeam.id
  }

  const target = state.objById(targetId)
  debug.locate(target)

  direction = unit.coords.directionTo(target.coords)

  if (unit.coords.distanceTo(target.coords) === 1) {
    // we're right next to them
    return Action.attack(direction)
  } else {
    return Action.move(direction)
  }
}

We can improve this code by taking advantage of init_turn(), which allows us to separate out the initialization code that needs to be called once every turn:

Python

target_id = None

def init_turn(state):
    global target_id

    if target_id:
        if not state.obj_by_id(target_id):
            # target has been defeated
            target_id = None

    if not target_id:
        allies = state.objs_by_team(state.our_team)

        def total_distance_for_team(enemy):
            return sum([ally.coords.distance_to(enemy.coords) for ally in allies])

        enemies = state.objs_by_team(state.other_team)
        closest_enemy_for_team = min(enemies,
            key=total_distance_for_team
        )
        target_id = closest_enemy_for_team.id

def robot(state, unit):
    target = state.obj_by_id(target_id)
    debug.locate(target)
    direction = unit.coords.direction_to(target.coords)

    if unit.coords.distance_to(target.coords) == 1:
        # we're right next to them
        return Action.attack(direction)
    else:
        return Action.move(direction)

Javascript

let targetId = null

function initTurn(state) {
  if (targetId) {
    if (!state.objById(targetId)) {
      // target has been defeated
      targetId = null
    }
  }

  if (!targetId) {
    allies = state.objsByTeam(state.ourTeam)

    const totalDistanceForTeam = (enemy) =>
      _.sum(allies.map(ally => ally.coords.distanceTo(enemy.coords)))

    enemies = state.objsByTeam(state.otherTeam)
    closestEnemyForTeam = _.minBy(enemies, totalDistanceForTeam)
    targetId = closestEnemyForTeam.id
  }
}

function robot(state, unit) {
  const target = state.objById(targetId)
  debug.locate(target)

  direction = unit.coords.directionTo(target.coords)

  if (unit.coords.distanceTo(target.coords) === 1) {
    // we're right next to them
    return Action.attack(direction)
  } else {
    return Action.move(direction)
  }
}

As you can probably tell, there are about a million ways to further improve this program. The stdlib may seem somewhat minimalist, but it actually contains more than enough tools for you to create arena-wrecking champions. Go create!

What’s next?

After you’ve completed your robot, in order for it to compete, you need to publish it!

If you run into problems, make sure to check out the tools available for debugging your robot.

Once you’re ready to move your robot development to a local IDE, you’ll need to meet rumblebot, our CLI tool.