Patterns as Data — Oscillators, Spaceships and Gliders
Once we can run Conway’s Game of Life, the next step is not to add more graphics.
It is to make the patterns themselves inspectable.
A blinker is not interesting because somebody named it.
It is interesting because it returns to the same state after two generations — a testable dynamical property, not an aesthetic judgment. Throughout this chapter, “interesting” always cashes out into something executable: a period, a displacement, a fingerprint match.
A glider is not interesting because it has a familiar shape.
It is interesting because the same local configuration reappears after several generations at a translated position.
Those are properties we can test.
The names themselves — still life, oscillator, spaceship, Methuselah — are community-standard vocabulary from five decades of Life study, and the community archive holds thousands of cataloged examples. Names are conveniences for pointing at behaviors. The behaviors are what we classify, and every classifier below tests behavior, never shape.
Store patterns explicitly
import numpy as np
PATTERNS = {
"block": np.array([
[1, 1],
[1, 1],
], dtype=np.uint8),
"blinker": np.array([
[1, 1, 1],
], dtype=np.uint8),
"glider": np.array([
[0, 1, 0],
[0, 0, 1],
[1, 1, 1],
], dtype=np.uint8),
}
The canonical figure for this chapter is generated from exactly those three patterns:
python scripts/figures/cellular-automata/part01_foundations.py 06

Now create a helper that places a pattern into a larger world:
def place(grid, pattern, row, col):
h, w = pattern.shape
grid[row:row+h, col:col+w] = pattern
This tiny abstraction changes our workflow.
We can now construct experiments from named initial conditions instead of repeatedly editing coordinates.
Three patterns are enough for this chapter because each teaches one detector: fixed point, period, translation. Famous-pattern encyclopedias exist elsewhere; here the library stays minimal on purpose.
Crop a pattern to its live bounding box
To compare patterns independently of empty space, crop away dead borders:
def crop_live(grid):
rows, cols = np.where(grid == 1)
if len(rows) == 0:
return np.zeros((0, 0), dtype=np.uint8)
return grid[
rows.min():rows.max() + 1,
cols.min():cols.max() + 1,
]
That lets us compare the intrinsic pattern rather than its absolute position.
There is an important limitation: cropping removes location but not rotation or reflection. That is fine for our first classifier, as long as we know exactly what invariance we have introduced.
Detect a still life
A still life is simply a fixed point:
F(state) = state
Test it directly:
def is_still_life(grid):
return np.array_equal(grid, life_step(grid))
A block should pass — and does (verified: a placed 2×2 block is a fixed point of life_step).
This is the first important shift from visual inspection to executable classification.
Detect an oscillator period
def oscillator_period(grid, max_period=20):
initial = grid.copy()
state = grid.copy()
for period in range(1, max_period + 1):
state = life_step(state)
if np.array_equal(state, initial):
return period
return None
For a blinker placed in a sufficiently large world:
period = oscillator_period(grid)
print(period)
should produce:
2
Two facts about this detector are worth stating plainly. First, a still life is period 1 — oscillator_period returns 1 for the block, so the still-life test is the period test with the tightest possible outcome, not a separate kind of magic. Second, the phrase “sufficiently large” matters because our life_step() currently uses periodic boundaries. If a pattern reaches an edge, wraparound becomes part of the experiment.
A third caveat belongs to every finite-window classifier: None means “no return within the window,” not “never returns.” A long transient — the R-pentomino, for example, correctly reports no period within 20 steps — can outlast any fixed bound. Absence of detection is a statement about the observation budget, and later measurement chapters will treat window choice as part of the experiment.
The definition has become code.
Detect translation
A spaceship repeats its shape after moving.
One simple approach is to crop the live cells:
def normalized_shape(grid):
return crop_live(grid)
Then track the top-left position of the live bounding box:
def live_origin(grid):
rows, cols = np.where(grid == 1)
if len(rows) == 0:
return None
return int(rows.min()), int(cols.min())
Now evolve until the shape repeats:
def find_translation_cycle(grid, max_steps=20):
initial_shape = normalized_shape(grid)
initial_origin = live_origin(grid)
state = grid.copy()
for step_number in range(1, max_steps + 1):
state = life_step(state)
if np.array_equal(normalized_shape(state), initial_shape):
origin = live_origin(state)
displacement = (
origin[0] - initial_origin[0],
origin[1] - initial_origin[1],
)
return step_number, displacement
return None
For our glider orientation, this reports the key dynamical fact (verified in code):
period = 4
displacement = (1, 1)
The exact displacement sign depends on the orientation and coordinate convention, so the reusable classifier should report it rather than hard-code a verbal direction.
Why this matters
The grid contains only bits.
Our analysis layer introduces concepts such as:
object identity
period
velocity
persistence
collision
Those concepts are not stored inside individual cells.
They are descriptions of patterns across space and time — observer-defined features, validated by the detectors above rather than exposed by the automaton itself.
This is an important general technique:
When a low-level system develops recurring structure, build measurements at the level where the recurring structure exists.
Do not force every useful concept into the primitive representation.
The detectors built above form one pipeline — each stage answers a narrower question than the last:
flowchart LR
H[history] --> C[crop to live box]
C --> S{returns unchanged?}
S -->|yes| ST[still life]
S -->|no| P{returns in place?}
P -->|yes| O[oscillator + period]
P -->|no| T[shape repeats shifted?]
T -->|yes| SP[spaceship + displacement]
T -->|no| TR[transient: unclassified in window]
To keep the vocabulary honest, the chapter distinguishes four things that are easy to blur:
pattern name (glider — a community convenience for pointing)
pattern behavior (returns shifted after 4 steps — tested above)
dynamical class (spaceship — behavior with nonzero displacement)
observer feature (velocity — our description, not a cell property)
Names point, behaviors test, classes group, features describe. Confusion starts when a name is treated as an explanation.
The classes, their tests, and what fools each test:
| Dynamical class | Operational test | Example | What fools it |
|---|---|---|---|
| Still life | is_still_life: fixed point | block (period 1) | nothing — exact test |
| Oscillator | oscillator_period: returns in place | blinker (period 2) | transients longer than the window |
| Spaceship | find_translation_cycle: repeats shifted | glider (4 steps, +(1,1)) | wraparound on a small torus |
| Transient | no return within window | R-pentomino (unclassified in 20) | any fixed window is a budget, not a verdict |
Collision experiments
Once patterns are data, we can generate experiments systematically.
def make_world(height=80, width=120):
return np.zeros((height, width), dtype=np.uint8)
world = make_world()
place(world, PATTERNS["glider"], 10, 10)
place(world, np.fliplr(PATTERNS["glider"]), 10, 80)
Now simulate and record:
history = simulate_life(world, 200)
We can ask:
- do the patterns survive?
- how many live cells remain?
- does the final state become periodic?
- are new moving structures emitted?
A collision therefore becomes a reproducible experiment rather than an animation we happened to watch once.
Pattern fingerprints
We can create a basic fingerprint:
def pattern_fingerprint(grid):
cropped = crop_live(grid)
return (
cropped.shape,
int(cropped.sum()),
cropped.tobytes(),
)
(Named for static patterns to distinguish it from Chapter 19’s behavioral fingerprint over trajectories — same “compress identity to comparable form” idea, different object, explicit name.)
Block, blinker, and glider fingerprints are pairwise distinct (verified), so the fingerprint at least separates our three teaching patterns.
For rotation- or reflection-invariant matching, generate transformed versions and choose a canonical representation.
For translation-invariant temporal matching, compare cropped shapes while separately retaining origin and time.
Those distinctions matter because “same pattern” is not one universal equivalence relation. We have to define what changes we want the identity test to ignore.
That gives us the beginning of a pattern database.
Later, similar ideas will help us catalog emergent structures in less familiar automata where nobody has already supplied names.
From named objects back to rules
It is tempting to look at Life and think the interesting things are gliders, blinkers and spaceships.
But remember the causal direction:
B3/S23
|
local updates
|
recurring structures
|
our higher-level names
The objects are consequences of the rule.
That means changing the rule can create an entirely different ecology of structures.
In the next chapter we will do exactly that: keep the same grid and neighborhood, but move beyond Conway’s Life into other Life-like cellular automata and multi-state rules.
Research
Berto, F. & Tagliabue, J. — Cellular Automata (Stanford Encyclopedia of Philosophy), §§2.5–2.6. Corroborates that still life, oscillator, glider/spaceship, and Methuselah are standard dynamical categories rather than this chapter’s inventions, and frames the analysis move this chapter implements: high-level descriptions of emergent patterns validated against the low-level rule. https://plato.stanford.edu/entries/cellular-automata/
LifeWiki (Conway Life community archive). The thousand-plus-pattern reference this chapter deliberately refuses to duplicate: hundreds of cataloged oscillators, spaceships, and still lifes with periods, displacements, and downloadable files. Use it when a behavior needs a name — and keep testing the behavior rather than trusting the name. https://conwaylife.com/wiki/