Escaping glider
inspired by Eric S. Raymond, Hacker emblem2
One glider escapes up and to the right from a burnt-out Game of Life soup, three fading echoes behind it.
Made with Claude Opus 5.5
- Technique
- simulations, pixel art
- Shape
- Any screen
- Added
- 27 September 2026
Colours
- #1C1B1Abackground
- #DAD8CEforeground
- #CF6A4Caccent
Export
FormatThis browser can’t make WebP files.
Shapecropped from 16:9
Crop
SizeThis browser can’t draw a file that large.
Notes
A random soup of cells ran for 1,500 generations of Conway’s rules. Anything still changing was then cleared, which leaves still lifes and blinkers, and only the largest cluster was kept. A glider moves one cell diagonally every four generations, so its echoes show where it was 12, 24 and 36 generations earlier.
Sources
- John Horton Conway, Conway’s Game of Life, 1970.
- Eric S. Raymond, Hacker emblem. ↑
Source code
wallpapers/glider/design.py, 88 lines
"""A Game of Life soup burnt down to still lifes and blinkers in rounded cells, with one glider escaping up and to the right, trailing three ghosts."""
import numpy as np
from numpy.typing import NDArray
from scipy.ndimage import binary_dilation, label
from walldye import (
ACCENT,
ACCENT_5,
ACCENT_6,
ACCENT_7,
BG_ALT,
UI,
Canvas,
NpRng,
P,
Path,
Vec,
design,
)
CELL, SQ, RAD = 14, 11, 2 # grid pitch, square side, corner radius
INSET = (CELL - SQ) / 2
# The soup runs on a fixed field, so every screen shape gets the same ash.
COLS, ROWS = 137, 77
PAD = 120 # dead margin so escaping gliders die far from the soup
SOUP = (42, 46, 40, 26) # ellipse centre and radii, in cells
GENS = 1500
LINK = 2 # cells; debris further than 2 * LINK from the main island is dropped
# (col, row) cells of the glider in the phase that travels up and to the right
GLIDER = np.array(
[(x, y) for y, row in enumerate(("OOO", "..O", ".O.")) for x, c in enumerate(row) if c == "O"]
)
# The glider, then ghosts three periods (12 generations, 3 cells) apart, so each echo stands clear.
TRAIL = (ACCENT, ACCENT_5, ACCENT_6, ACCENT_7)
def life(g: NDArray[np.bool_]) -> NDArray[np.bool_]:
"""One Game of Life generation of `g`; cells beyond its edge stay dead."""
p = np.pad(g, 1).astype(np.uint8)
h, w = g.shape
n = np.zeros((h, w), np.uint8)
for dy in range(3):
for dx in range(3):
if dy != 1 or dx != 1:
n += p[dy : dy + h, dx : dx + w]
return (n == 3) | (g & (n == 2))
def ash(rng: NpRng) -> NDArray[np.intp]:
"""The settled soup's largest island as (col, row) cells about its bounding-box centre."""
g = np.zeros((ROWS + 2 * PAD, COLS + 2 * PAD), bool)
yy, xx = np.mgrid[:ROWS, :COLS]
r = ((xx - SOUP[0]) / SOUP[2]) ** 2 + ((yy - SOUP[1]) / SOUP[3]) ** 2
g[PAD : PAD + ROWS, PAD : PAD + COLS] = (r < 1) & (rng.random((ROWS, COLS)) < 0.4)
for _ in range(GENS):
g = life(g)
g &= life(life(g)) # keep only still lifes and blinkers
g = g[PAD : PAD + ROWS, PAD : PAD + COLS] & (r < 1.1)
lab, _ = label(g, np.ones((3, 3), bool))
g &= np.bincount(lab.ravel())[lab] >= 3 # lone cells cut from passing gliders
# keep one island: the largest group of objects within 2 * LINK cells of each other
near, _ = label(binary_dilation(g, np.ones((2 * LINK + 1,) * 2, bool)))
sizes = np.bincount(near.ravel(), weights=g.ravel())
sizes[0] = 0
ys, xs = np.nonzero(g & (near == sizes.argmax()))
return np.column_stack((xs - (xs.min() + xs.max()) // 2, ys - (ys.min() + ys.max()) // 2))
def squares(cells: NDArray[np.intp], at: Vec) -> Path:
"""A rounded square centred in each (col, row) cell of the grid whose cell (0, 0) starts at `at`."""
d = P()
for col, row in cells:
d.rrect(at.x + col * CELL + INSET, at.y + row * CELL + INSET, SQ, SQ, RAD)
return d
@design(aspects="any")
def draw(s: Canvas) -> None:
with s.pattern(CELL, CELL) as board:
board.fill(P().circle((CELL / 2, CELL / 2), 0.9), BG_ALT)
s.fill(P().rect(0, 0, s.w, s.h), board.ref)
# cell corners on the board's grid: the island low on the left, the glider up to its right
island = s.pick(landscape=(0.299, 0.609), portrait=(0.44, 0.64), snap=CELL)
glider = s.pick(landscape=(0.766, 0.363), portrait=(0.72, 0.29), snap=CELL)
s.fill(squares(ash(s.np_rng(29)), island), UI)
for k, tone in enumerate(TRAIL):
s.fill(squares(GLIDER, glider + (-3 * k * CELL, 3 * k * CELL)), tone)"""A Game of Life soup burnt down to still lifes and blinkers in rounded cells, with one glider escaping up and to the right, trailing three ghosts."""
import numpy as np
from numpy.typing import NDArray
from scipy.ndimage import binary_dilation, label
from walldye import (
ACCENT,
ACCENT_5,
ACCENT_6,
ACCENT_7,
BG_ALT,
UI,
Canvas,
NpRng,
P,
Path,
Vec,
design,
)
CELL, SQ, RAD = 14, 11, 2 # grid pitch, square side, corner radius
INSET = (CELL - SQ) / 2
# The soup runs on a fixed field, so every screen shape gets the same ash.
COLS, ROWS = 137, 77
PAD = 120 # dead margin so escaping gliders die far from the soup
SOUP = (42, 46, 40, 26) # ellipse centre and radii, in cells
GENS = 1500
LINK = 2 # cells; debris further than 2 * LINK from the main island is dropped
# (col, row) cells of the glider in the phase that travels up and to the right
GLIDER = np.array(
[(x, y) for y, row in enumerate(("OOO", "..O", ".O.")) for x, c in enumerate(row) if c == "O"]
)
# The glider, then ghosts three periods (12 generations, 3 cells) apart, so each echo stands clear.
TRAIL = (ACCENT, ACCENT_5, ACCENT_6, ACCENT_7)
def life(g: NDArray[np.bool_]) -> NDArray[np.bool_]:
"""One Game of Life generation of `g`; cells beyond its edge stay dead."""
p = np.pad(g, 1).astype(np.uint8)
h, w = g.shape
n = np.zeros((h, w), np.uint8)
for dy in range(3):
for dx in range(3):
if dy != 1 or dx != 1:
n += p[dy : dy + h, dx : dx + w]
return (n == 3) | (g & (n == 2))
def ash(rng: NpRng) -> NDArray[np.intp]:
"""The settled soup's largest island as (col, row) cells about its bounding-box centre."""
g = np.zeros((ROWS + 2 * PAD, COLS + 2 * PAD), bool)
yy, xx = np.mgrid[:ROWS, :COLS]
r = ((xx - SOUP[0]) / SOUP[2]) ** 2 + ((yy - SOUP[1]) / SOUP[3]) ** 2
g[PAD : PAD + ROWS, PAD : PAD + COLS] = (r < 1) & (rng.random((ROWS, COLS)) < 0.4)
for _ in range(GENS):
g = life(g)
g &= life(life(g)) # keep only still lifes and blinkers
g = g[PAD : PAD + ROWS, PAD : PAD + COLS] & (r < 1.1)
lab, _ = label(g, np.ones((3, 3), bool))
g &= np.bincount(lab.ravel())[lab] >= 3 # lone cells cut from passing gliders
# keep one island: the largest group of objects within 2 * LINK cells of each other
near, _ = label(binary_dilation(g, np.ones((2 * LINK + 1,) * 2, bool)))
sizes = np.bincount(near.ravel(), weights=g.ravel())
sizes[0] = 0
ys, xs = np.nonzero(g & (near == sizes.argmax()))
return np.column_stack((xs - (xs.min() + xs.max()) // 2, ys - (ys.min() + ys.max()) // 2))
def squares(cells: NDArray[np.intp], at: Vec) -> Path:
"""A rounded square centred in each (col, row) cell of the grid whose cell (0, 0) starts at `at`."""
d = P()
for col, row in cells:
d.rrect(at.x + col * CELL + INSET, at.y + row * CELL + INSET, SQ, SQ, RAD)
return d
@design(aspects="any")
def draw(s: Canvas) -> None:
with s.pattern(CELL, CELL) as board:
board.fill(P().circle((CELL / 2, CELL / 2), 0.9), BG_ALT)
s.fill(P().rect(0, 0, s.w, s.h), board.ref)
# cell corners on the board's grid: the island low on the left, the glider up to its right
island = s.pick(landscape=(0.299, 0.609), portrait=(0.44, 0.64), snap=CELL)
glider = s.pick(landscape=(0.766, 0.363), portrait=(0.72, 0.29), snap=CELL)
s.fill(squares(ash(s.np_rng(29)), island), UI)
for k, tone in enumerate(TRAIL):
s.fill(squares(GLIDER, glider + (-3 * k * CELL, 3 * k * CELL)), tone)
Run it yourself
$ git clone https://github.com/nickolaj-jepsen/walldye && cd walldye$ uv run walldye render glider --theme fireproof -o glider-fireproof-16x9.svg