Learn to Program

nano-bot — Habitas Games
No experience neededPython8 lessons

Most people learn to program by printing words to a black screen. You're going to learn by writing the mind of a microscopic swarm that has to save a patient without you.

Who this is for
Someone who has never written a line of Python — or has written a little and wants it to actually do something. Every concept here is introduced because the bots need it, and you can watch each one work. If you already know Python, skip straight to the strategy tutorial.

The lessons

  1. Get it running
  2. Giving one order — functions and arguments
  3. Naming things — variables and coordinates
  4. Choosingif and else
  5. Doing it to everyonefor loops
  6. Collections — lists, objects and dictionaries
  7. Your own instructions — writing functions
  8. Remembering — state that survives the turn
  9. Your first real swarm — putting it together

Setup Get it running

You need Python 3.10 or newer. Then, in a terminal, from the project folder:

python3 -m venv .venv
source .venv/bin/activate      # Windows: .venv\Scripts\activate
pip install -r requirements.txt

python main.py                 # opens the app

Make yourself a file to work in — strategies/my_strategy.py. Everything you write in these lessons goes in that one file.

To see it run: in the app, press Run Match, pick a map and pick my_strategy for Player 1, then press Run Match again. You'll watch the whole thing play back.

The single most useful habit
When your bots do nothing and you don't know why, look at the Events panel in the match window. If your code has an error, it says so there — with the actual error message — instead of failing silently.

Lesson 1 Giving one order

A program is a list of instructions. In Python you give an instruction by writing its name followed by brackets. That's called calling a function:

bot.stop()

stop is the instruction. The empty brackets () mean "do it now." Some instructions need extra information to make sense — "move" is meaningless without where. Information you hand a function is called an argument, and it goes inside the brackets:

bot.move_to((10, 4))     # go to column 10, row 4

Here is the smallest complete program that does something. Type it into your file:

from nanobot.api.nano_strategy import NanoStrategy


class MyStrategy(NanoStrategy):
    def choose_injection_point(self, map_info):
        return (0, 0)

    def what_to_do_next(self, map_info, my_bots):
        for bot in my_bots:
            bot.move_to((10, 10))

Don't worry about every word yet. The important part: what_to_do_next is your instruction list, and the game runs it once every turn, 1500 times. You are not describing one moment — you are describing what to do at any moment.

Try it
Run it and watch. Your bot walks to cell (10, 10) and stops. Now change the numbers and run again. Coordinates start at (0, 0) in the top-left corner.

Lesson 2 Naming things

Writing (10, 10) everywhere is fragile — change your mind and you must find every copy. Instead give the value a name. That's a variable:

destination = (10, 10)
bot.move_to(destination)

The = means "store this under this name" (it is not the maths "equals"). Now the value lives in one place.

That (10, 10) with brackets and a comma is a tuple — an ordered pair. Every position in this game is one: (x, y), column then row. You read the pieces out by position, counting from zero:

spot = (10, 4)
x = spot[0]      # 10
y = spot[1]      # 4

Your bots carry information you can read the same way, using a dot:

bot.position     # where it is, e.g. (3, 7)
bot.hp           # how much health it has left
bot.azn          # how much medicine it's carrying
bot.type         # what kind of bot it is, e.g. "NanoCollector"
Try it
Make your bot walk to a spot four cells right of where it started: x, y = bot.position then bot.move_to((x + 4, y)).

Lesson 3 Choosing

Real programs make decisions. In Python that's if: do this only when something is true.

if map_info.azn_bank >= 20:
    # we can afford a collector (they cost 20)
    ai.build("NanoCollector", (1, 2))

Two things matter here. The colon at the end of the line, and the indentation underneath it. In Python, indented lines are the ones that belong to the if. Indentation isn't decoration — it's how Python knows what goes with what. Four spaces per level, consistently.

You can add alternatives with elif ("otherwise, if…") and else:

if bot.azn >= 10:
    bot.move_to(needle.position)      # carrying enough — deliver it
elif bot.hp < 20:
    bot.stop()                        # hurt — sit still
else:
    bot.move_to(node.position)        # otherwise go mine

Comparisons you'll use constantly:

WrittenMeans
==is equal to (two equals signs — one means "store")
!=is not equal to
>= <=at least / at most
and orboth must be true / either will do
is Nonethere's nothing here at all
The classic beginner bug
= stores a value. == compares two. Writing if x = 5 is an error; you want if x == 5.

Lesson 4 Doing it to everyone

You don't have one bot, you have a swarm — and it grows. You can't write a line per bot. A loop repeats the same instructions for each item in a group:

for bot in my_bots:
    bot.stop()

Read it as an English sentence: "for each bot in my bots, stop it." The name bot is yours to choose — it just holds one item at a time while the indented lines run.

Loops combine with if to act on only some of them:

for bot in my_bots:
    if bot.type == "NanoCollector":
        bot.move_to((20, 20))    # only the collectors move
One order per bot per turn
Each bot obeys exactly one instruction each turn, and if you give it two, the last one wins. Calling move_to and then stop on the same bot in the same turn means it just stops. This surprises everyone once.

Lesson 5 Collections

my_bots is a list — several things in order. The map hands you more of them:

map_info.habitas_points   # the treatment sites
map_info.azn_nodes        # where the medicine is
map_info.visible_enemies   # rival bots you can currently see

You can check how many there are, or grab one:

len(map_info.azn_nodes)     # how many nodes exist
map_info.azn_nodes[0]        # the first one (counting starts at 0)

The items inside are objects — bundles of related facts you read with a dot, like bots:

node = map_info.azn_nodes[0]
node.position     # (x, y)
node.quantity     # how much medicine is left in it

point = map_info.habitas_points[0]
point.position
point.owner_id    # -1 means nobody has claimed it yet

One exception worth memorising, because it trips people up: visible enemies and hazards are dictionaries, not objects. A dictionary looks things up by name in square brackets with quotes:

enemy = map_info.visible_enemies[0]
enemy["position"]      # square brackets + quotes, NOT enemy.position
enemy["hp"]

Finally, you can build a smaller list from a bigger one by filtering it:

free = [p for p in map_info.habitas_points if p.owner_id == -1]

That reads: "every point p in the list, but only where nobody owns it."

Lesson 6 Your own instructions

When you catch yourself writing the same few lines twice, give them a name of their own. That's def — defining a function:

    def distance(self, a, b):
        return abs(a[0] - b[0]) + abs(a[1] - b[1])

return hands an answer back to whoever asked. Now you can use it anywhere: self.distance(bot.position, node.position). (Inside a class every function takes self first, and you call your own with self. in front — for now, just follow the pattern.)

Here's a genuinely useful one — find the closest thing in a list:

    def nearest_node(self, map_info, from_pos):
        best = None
        for node in map_info.azn_nodes:
            if node.quantity == 0:
                continue                  # empty — skip to the next one
            if best is None or self.distance(from_pos, node.position) < self.distance(from_pos, best.position):
                best = node
        return best

Follow the idea: remember the best one seen so far, look at each in turn, replace the champion whenever you find a closer one. That "keep the best so far" pattern is everywhere in programming.

Lesson 7 Remembering

what_to_do_next runs from scratch every turn. Ordinary variables inside it vanish when it ends — so how do you remember something between turns?

You store it on self, which is your strategy object and sticks around for the whole match. Set up what you want to remember in __init__:

    def __init__(self):
        self.home = None

    def what_to_do_next(self, map_info, my_bots):
        ai = my_bots[0]
        if self.home is None:
            self.home = ai.position     # first turn only: remember where we started

This matters more than it looks. Deciding something once and sticking to it is often the difference between a swarm that works and one that dithers: if you re-pick the "nearest" node every single turn, two nodes at almost equal distance will swap places as your bot moves, and it will walk back and forth between them forever without ever arriving. Real strategies hit this bug constantly. Remembering your choice fixes it.

Lesson 8 Your first real swarm

Everything so far, in one working strategy. It builds a collector, claims a treatment site, and ferries medicine into it — which is the entire core loop of the game.

from nanobot.api.nano_strategy import NanoStrategy


class MyStrategy(NanoStrategy):
    def choose_injection_point(self, map_info):
        return (0, 0)

    def distance(self, a, b):
        return abs(a[0] - b[0]) + abs(a[1] - b[1])

    def find(self, my_bots, kind):
        for bot in my_bots:
            if bot.type == kind and bot.is_alive:
                return bot
        return None

    def what_to_do_next(self, map_info, my_bots):
        ai      = self.find(my_bots, "NanoAI")
        worker  = self.find(my_bots, "NanoCollector")
        implant = self.find(my_bots, "NanoNeedle")
        if ai is None:
            return

        # 1. No worker yet? Build one next to the commander.
        if worker is None and map_info.azn_bank >= 20:
            x, y = ai.position
            for spot in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)):
                cell = map_info.get_cell(spot[0], spot[1])
                if cell is not None and not cell.is_bone:
                    ai.build("NanoCollector", spot)
                    break

        # 2. No implant yet? Walk to a free site and plant one ON it.
        elif implant is None:
            free = [p for p in map_info.habitas_points if p.owner_id == -1]
            if free:
                site = free[0].position
                for p in free:
                    if self.distance(ai.position, p.position) < self.distance(ai.position, site):
                        site = p.position
                if self.distance(ai.position, site) == 1:
                    if map_info.azn_bank >= 40:
                        ai.build("NanoNeedle", site)
                else:
                    ai.move_to(site)

        # 3. The worker: carry medicine in, or go fetch more.
        if worker is not None:
            node = None
            for n in map_info.azn_nodes:
                if n.quantity > 0:
                    if node is None or self.distance(worker.position, n.position) < self.distance(worker.position, node.position):
                        node = n

            if implant is not None and worker.azn >= 10:
                if worker.position == implant.position:
                    worker.transfer_to(implant.position)
                else:
                    worker.move_to(implant.position)
            elif node is not None:
                if worker.position == node.position:
                    worker.collect_from(node.position)
                else:
                    worker.move_to(node.position)
This is verified
That exact code was extracted from this page and run against all three shipped maps: it claims a site, fills the implant to 90–100 AZN, scores 200–220 points per turn, and wins on every map against the starter example. It is not a toy — it's a real, functioning strategy.
The rule that catches everyone
collect_from() and transfer_to() only work while your bot is standing on that exact cell. There is no picking things up from a distance — move_to there first, then act.

What you actually just learned

Not "nano-bot syntax." These are the load-bearing ideas in every programming language:

You usedIt's calledYou'll meet it in
bot.move_to((10, 4))calling a function with argumentseverything, forever
x = spot[0]variables and indexingevery language
if / elif / elseconditionalsevery language
for bot in my_bots:iterationevery language
[p for p in … if …]filtering a collectionSQL, spreadsheets, data science
def nearest_node(…)defining functions, "best so far"every algorithm you'll write
self.homeobject stateall object-oriented code

And one lesson most beginners take years to meet: your program has to keep working when you aren't watching. Your swarm runs 1500 turns without you. It will face situations you never imagined, and it has to do something sensible anyway. That is the actual job of a programmer, and you've now done it.

Where to go next