EntLab

Writing rulesets

A ruleset is one .py file holding the law of nature for a world. Whatever it does to world is the rule. There is nothing else going on behind it.

The shape of a ruleset

DESCRIPTION = "one line shown in the rulesets menu"   # optional

def step(world):        # required name, required single argument
    ...                 # advance the world by exactly one tick

EntLab calls step(world) once per tick while playing, and once per press of N.

Start with the tutorial. Select the tutorial ruleset in Customize rulesets, create a world, then open rulesets/tutorial.py in any text editor and follow the experiments written into it. template.py is a clean file to copy, and New ruleset from template does exactly that.

Nine rulesets ship with EntLab: tutorial, conway, replicator, maze, seeds, life_without_death, gravity, langtons_ant, and template. Read them all →

The edit and test loop

You pick a ruleset when you create a world, and a full copy of its source goes inside every saved .grid. That means editing a library file never quietly changes a world you already made.

When you do want the change, use the Esc menu inside a world:

  • Change ruleset… swaps the law of nature while the world runs. Watch the same population meet a different physics.
  • reload re-reads your library file after you have edited it. That is the loop: edit, reload, play.
  • Dragging a .py onto the window does the same thing.

Saves always embed whatever the world is running right now.

Recipe 1: any Life-like rule in one line

For a rule "born on neighbour counts B, survives on counts S":

def step(world):
    n = world.neighbor_counts()
    a = world.cells
    world.cells[:] = (~a & (n == 3)) | (a & ((n == 2) | (n == 3)))
    #                 ^ birth: dead, n in B    ^ survival: alive, n in S

Swap the comparisons and you have any of these:

ruleB / Scharacter
Conway's LifeB3 / S23the classic
HighLifeB36 / S23Life plus a natural replicator
Day & NightB3678 / S34678symmetric in black and white
ReplicatorB1357 / S1357everything copies itself (world.cells[:] = n % 2 == 1)
SeedsB2 / S∅everything dies each tick, patterns still spread
MazeB3 / S12345grows corridors
Life without DeathB3 / S012345678coral, frost
Anneal (majority)B4678 / S35678blobs smooth out like surface tension
DiamoebaB35678 / S5678amoebas with straight edges

Recipe 2: randomness

Always use world.rng rather than the global random, so a world replays the same way twice:

def step(world):
    if world.rng.random() < 0.3:
        world.set(world.rng.randint(1, world.width),
                  world.rng.randint(1, world.height))

Recipe 3: memory between ticks

world.state is a dict that survives from one tick to the next and is saved inside .grid files. It is how you write walkers, phases and counters, meaning anything that depends on more than the current board.

def step(world):
    phase = world.state.setdefault("phase", 0)
    if phase == 0 and world.count > 500:
        world.state["phase"] = 1      # e.g. switch laws mid-history
    ...

Keep it to things JSON can hold (numbers, strings, booleans, lists, dicts). Anything else is dropped when you save, so store a NumPy array as arr.tolist(). The whole of rulesets/langtons_ant.py is three keys in world.state.

Recipe 4: working on the whole grid at once

world.cells is a live NumPy array, and slicing it is how you write fast rules with a direction to them, which is not something a normal cellular automaton can do. In rulesets/gravity.py, Ents fall and stack:

def step(world):
    c = world.cells                      # c[x, y]; y index 0 = bottom row
    falling = c[:, 1:] & ~c[:, :-1]      # an Ent with an empty cell below
    c[:, 1:] &= ~falling                 # vacate the old cell
    c[:, :-1] |= falling                 # land one row down

Shift left and right with c[1:, :] and c[:-1, :] the same way.

When you replace the whole grid, write world.cells[:] = new rather than world.cells = new. The first writes into the array that is already there, so it cannot change its shape or type by accident.

When your ruleset breaks

A crash inside step() never kills the app. The world pauses and a red banner names the error and the line it died on. details opens the whole message, with buttons to copy it or jump into the ruleset, and the full traceback goes to logs/entlab.log (Settings → Open logs). Fix the file, then Esc → reload to pull your fix into the running world, and press play again.

The usual culprits:

  • step(world) has to exist and be a function. Until it does, the menu marks the file red and won't let you pick it.
  • Coordinates off the board don't raise an error: get returns False and set does nothing. Check in_bounds when a walker might march off the edge.
  • world.state has to stay JSON-friendly or it won't survive save and load.
  • Don't hold on to world.cells between ticks. Undo and load can swap the array out from under you.