game_of_life_text

booting python

github

booting python

gen0 live0 board0×0

waiting for python

Words made of still lifes

Every character here comes from a 5x7 bitmap font. Each lit pixel of a glyph becomes a 2x2 block, the smallest still life in Conway's Game of Life, so the finished word sits on the board unchanged instead of decaying the moment the simulation runs. Nothing paints those blocks directly. All of them are built by gliders launched from outside the text, colliding in the right place at the right time.

One block takes two gliders. Fire them on the right offset and four generations later they have eaten each other down to a single 2x2 block. That collision is the only building material in the app, and the real problem is scheduling a few hundred of them so that no glider ever clips a block that is already finished, or one that is still on its way in.

How the search works

Blocks are placed one at a time, sorted by distance from the centre of the text. The middle of a word is the hardest place to reach once the outside is standing, so it gets built first, while later gliders still have empty board to fly across.

Each block takes its launch direction from where it sits relative to that centre, so a block up and to the right is built by gliders arriving from up and to the right. The dominant axis is tried first and the other axis is kept as a fallback, which gives every block two orientations to work with.

Then there is the delay. A glider repeats its own shape every four generations, so a synthesis can be launched any whole number of periods early and still land on exactly the same square, just later. The planner walks these delay slots upward from zero, trying both orientations at each one, and keeps the first combination that fits. That is the whole search: the smallest delay that works, the first orientation that works, no backtracking. Text dense enough to exhaust the slot budget is rejected rather than approximated.

What keeps it quick is the test for whether a candidate fits. Re-simulating the board for every candidate would be far too slow, so the planner never simulates the combination at all. Instead it carries a running summary of the work already placed: every cell any construction touches at every generation, and how many live neighbours each dead cell is being handed at each generation. A candidate is accepted when two conditions hold.

Hold both and the two patterns laid on top of each other evolve exactly the way they evolve apart, which is what lets a slot be accepted after one comparison against the summary rather than a fresh run of the board. A cheap filter runs before either check: if the candidate never touches a cell that any placed block touches at any generation, the checks are vacuous and the slot is taken immediately.

Once every block has a slot the combined seed is simulated once, in full, and compared against the target. If a single cell is out of place the construction is thrown away instead of shown, so anything that reaches the canvas is known to settle exactly.

Exporting patterns

The export .rle button writes the current board as Run Length Encoded text, the usual interchange format for Life patterns, so a word built here opens unchanged in Golly or LifeViewer. Boards can also be seeded at random or left blank and drawn on by hand, with wrapping edges on or off.

How it runs

The canvas and the simulation loop are TypeScript. The planner is the same Python package that backs the desktop build, running under Pyodide in a worker thread, which is why the first visit spends a moment fetching a WebAssembly runtime and NumPy before the generate button lights up. After that it is cached.

The source, including the PySide6 desktop version, is on GitHub under the MIT licence.