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
.pyonto 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:
| rule | B / S | character |
|---|---|---|
| Conway's Life | B3 / S23 | the classic |
| HighLife | B36 / S23 | Life plus a natural replicator |
| Day & Night | B3678 / S34678 | symmetric in black and white |
| Replicator | B1357 / S1357 | everything copies itself (world.cells[:] = n % 2 == 1) |
| Seeds | B2 / S∅ | everything dies each tick, patterns still spread |
| Maze | B3 / S12345 | grows corridors |
| Life without Death | B3 / S012345678 | coral, frost |
| Anneal (majority) | B4678 / S35678 | blobs smooth out like surface tension |
| Diamoeba | B35678 / S5678 | amoebas 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:
getreturnsFalseandsetdoes nothing. Checkin_boundswhen a walker might march off the edge. world.statehas to stay JSON-friendly or it won't survive save and load.- Don't hold on to
world.cellsbetween ticks. Undo and load can swap the array out from under you.