Finding a Game of Life Pattern's Period, Programmatically
A verifiable recipe for detecting an oscillator or spaceship's period in Conway's Game of Life using our MCP run_generations tool — no eyeballing frames, no trusting a table.
Finding a Game of Life Pattern's Period, Programmatically
Our own guide to oscillators states that Octagon 2 has period 5. It doesn't say how anyone would find that out for a pattern nobody's classified yet — you'd have to watch it, count frames, and hope you didn't lose track around generation 40. That's exactly the kind of thing worth handing to a tool instead of a stopwatch, and it's exactly what our run_generations MCP tool is built for. Below is the actual recipe, tested live against five real patterns pulled from the library, including an independent re-check of that period-5 claim.
The idea in one sentence
Run the pattern forward for increasing generation counts — 1, 2, 3, … — starting from the original, untouched pattern each time, and stop at the first count whose resulting shape exactly reproduces the starting shape (allowing for the whole thing having slid to a new position). That count is the period.
Why the first match is the real period, not just a candidate
This isn't a heuristic that usually works — it's provably correct, and the reason is worth stating plainly: Game of Life is deterministic. The state at generation p+1 is a pure function of the state at generation p, nothing else. So if generation p is an exact copy of generation 0 (shifted, maybe, but otherwise identical), then generation 2p must be an exact copy of generation p too, by the same rule applied again — and so on forever. That means any generation count where the pattern returns to its start is automatically a multiple of the pattern's true minimal period. Scanning upward from 1 and stopping at the first hit can't overshoot: the first match you find is the smallest one, by construction. No factoring, no checking divisors, no second-guessing.
The recipe against our MCP server
The mechanics are simpler than the proof. For each candidate generation count p, call run_generations directly on the original pattern (not chained from a previous result — each check is an independent live simulation from generation 0), and compare the rle field it returns to the pattern's own starting RLE from get_pattern:
curl -X POST "https://life.angen.ai/api/mcp/mcp" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
"name":"run_generations",
"arguments":{"patternId":"pulsar","generations":3}
}}'
If the rle in the response matches the pattern's original rle exactly, p is the period. If not, try the next p.
Five patterns, checked live
Every row below is a real call against the production endpoint this run — not simulated, not carried over from a table.
| Pattern | Kind | First generation with an exact RLE match | Offset at that point |
|---|---|---|---|
| Block | still life | 1 | (0, 0) |
| Blinker | oscillator | 2 | (0, 0) |
| Pulsar | oscillator | 3 | (0, 0) |
| Glider | spaceship | 4 | (1, 1) |
| Octagon 2 | oscillator | 5 | (0, 0) |
A still life is just the degenerate case — period 1 means "matches itself after doing nothing," and the block's run_generations call for generations: 1 returns the identical 2o$2o! it started with. The blinker is more interesting: at generation 1 it's rotated 90° (3o! becomes o$o$o!), a completely different RLE string, and even the bounding box has shifted (offset: {dx: 1, dy: -1}) because a horizontal and vertical 3-cell line don't share a top-left corner. It only matches back at generation 2, offset (0, 0) — confirming period 2, not period 1, exactly as it should. Octagon 2 checks out at generation 5, independently confirming the older guide's claim rather than just repeating it — and generation 6 (not shown above) reproduces generation 1's shape exactly, which is a nice sanity check that the cycle is genuinely closing rather than coincidentally matching once.
Spaceships don't need special-case code
The glider row above is the one that looks like it should need extra handling, and it doesn't. A glider physically moves one cell diagonally every 4 generations — it never returns to the same coordinates — but run_generations' rle field is always encoded relative to the pattern's own bounding box, not absolute grid position, so the returned string at generation 4 is bo$2bo$3o!, byte-for-byte identical to the glider's starting RLE from get_pattern. The travel shows up entirely in the separate offset field: {dx: 1, dy: 1} per 4-generation cycle, which is the standard way to describe a diagonal spaceship's speed (a 1c/4 diagonal, in the usual notation). Comparing the raw rle strings is enough to detect the period; the offset is what tells you the pattern is a spaceship rather than a stationary oscillator, and in which direction it travels.
One caveat: this only works cleanly because get_pattern returns library patterns already canonically anchored to their own bounding box. If you're checking a hand-written starting RLE that isn't already anchored that way, run it through normalize_pattern first — otherwise a real period match could show up as a false mismatch simply because your input and the tool's bbox-relative output disagree about where "the corner" is.
When to stop looking
Not every pattern is periodic, and scanning forever isn't a plan. run_generations caps out at 1000 generations per call, but most named oscillators in the library have periods well under 150 — a first pass up to 20 or 50 catches nearly everything, and you only widen the search if you have a specific reason to (chasing a known high-period record-holder, for instance). Watch two other fields in the response instead of just grinding through generation counts: extinct (population hit zero — nothing left to oscillate) and aborted (population ran away and hit the simulation's population cap). Either one means "this isn't settling into a cycle," not "keep scanning higher." R-pentomino — a known methuselah — makes the point live: pushed to run_generations' own 1000-generation ceiling, it's still non-extinct, still non-aborted, population still 156 and climbing from its 5-cell start, with a bounding box now hundreds of cells wide (escaped gliders dragging it outward). That's not a slow oscillator whose period you haven't found yet. It's a pattern still finding its footing 1000 generations in, and the honest move at that point is to keep simulating for the eventual population, not to keep guessing at periods.
Try it yourself
Every call in this piece is reproducible against the same public, unauthenticated endpoint (https://life.angen.ai/api/mcp/mcp) — pick any pattern from the library, pull its RLE with get_pattern, and scan candidate periods with run_generations the same way. The MCP tutorial has the full setup for Claude Desktop, and the terminal recipe covers the same thing for Claude Code's CLI if you'd rather script it than chat through it. Or just watch Octagon 2 breathe in the simulator — five generations, on repeat, now provably five and not just listed as five.