Programs that discover their own primitives
M. Siper, A. Khalifa, and J. Togelius, “Procedural Content Metageneration via Program Search and Continual Abstraction Discovery,” in Proc. IEEE Conference on Games (CoG), 2026, to appear.
@inproceedings{siper2026metageneration,
author = {Siper, Matthew and Khalifa, Ahmed and Togelius, Julian},
title = {Procedural Content Metageneration via Program Search
and Continual Abstraction Discovery},
booktitle = {2026 IEEE Conference on Games (CoG)},
year = {2026},
publisher = {IEEE},
note = {To appear}
}
Continual Abstraction Discovery in one run: a generator evolves, a recurring block of reachability logic is lifted into helpers.py, and later candidates call it instead of rewriting it.
Most procedural content generation searches the space of artifacts: you evolve a level, score it, and evolve it again. Metageneration searches the generator itself.
A program can be compiled, executed, inspected, edited by a designer, and reused outside the search system. Large language models are now good enough at writing and revising code to act as the mutation and crossover operators inside an evolutionary loop.
Searching over raw code fixes the vocabulary that variation can draw on. As programs grow, useful routines for reachability, entity normalization, repair, and structural construction get independently reinvented in candidate after candidate.
Continual Abstraction Discovery (CAD) is the response: during the run, extract those recurring routines from high-fitness programs into a run-specific helper module, validate them, and refactor the source to call them. The searchable vocabulary grows while the population is still being optimized.
We use an evolutionary algorithm to search the space of code to find level generators for different games.
def generate(context_dict): """Receives grid dimensions and an initial level, returns a tile grid.""" w, h = context_dict["width"], context_dict["height"] level = Grid(w, h, fill=TILES.EMPTY) # local functions, constants and layout routines are free to vary return level
A parent is sampled from the archive with fitness-proportional probability. Mutation asks for a strong behavioural edit; crossover (probability 0.25) asks the model to combine compatible mechanisms from two parents into one executable program.
After every generation an LLM reflection call appends a structured entry to a per-run memory file: what was attempted, what happened, and the global lesson. That memory feeds later variation calls.
From generation 8, and every five generations after, CAD extracts reusable helpers from the programs at or above the 75th percentile of fitness. A helper is kept only if it compiles and passes a smoke test, and the program is refactored to call it only if behaviour is unchanged on a fixed-seed test. Survivors go into a run-specific module that later variation calls can use.
V is the fraction of sampled levels whose quality equals 1.0, Q is mean level quality, D is mean pairwise diversity. All terms lie in [0, 1]. Candidates that crash or fail validation receive fitness zero.
The program on the right is executed one line at a time. The level on the left is its actual state at that moment — a loop stays highlighted while its body runs, and every tile appears on the pass that placed it. Each of these programs was run under a line tracer, so what you see is the real execution, not an illustration of one. Every level ends with a playability check under that game's own movement rules.
The main study crosses CAD with access to a fixed, hand-written expert API. That separates having a reusable vocabulary from discovering one.
CAD raises mean final best fitness in all eight domain and API comparisons.
Independent runs converge on a recurring core of validation, reachability and structural operations — and then trail off into a long tail of run-specific utilities shaped by the domain and the search history.
| Abstraction | Category | Total calls |
|---|---|---|
| In Bounds | Other | 4,693 |
| Entity Count Normalization | Validation | 3,848 |
| Ensure One Player | Validation | 2,611 |
| Find Player Position | Utility | 2,370 |
| Ladder Placement | Structural | 768 |
| Flood Fill | Other | 569 |
| Find Tiles | Other | 574 |
| Grounded Empty Cells | Utility | 465 |
| Reachability BFS | Validation | 344 |
| Relocate Unreachable Entities | Validation | 320 |
| Connected Components | Other | 297 |
| Deep Copy Grid | Other | 118 |
Table I. Semantically equivalent functions are grouped; Total Calls is the aggregate call count in the supplied audit. in_bounds and Entity Count Normalization each appear in 28 of the CAD runs; Ensure One Player and Grounded Empty Cells appear in 24; Find Player Position, Ladder Placement, Reachability BFS, Relocate Unreachable Entities and connected_components each appear in 20.
The interesting cases are the domain-specific ones. In Lode Runner the search abstracted a function that places enemies beside gold, making levels harder — and subsequent generators kept calling it. Something similar happened in Dangerous Dave, where CAD produced a function that arranges hazards around the solution path, which then shaped how later generators placed objects.
M. Siper, A. Khalifa, and J. Togelius, “Procedural Content Metageneration via Program Search and Continual Abstraction Discovery,” in Proc. IEEE Conference on Games (CoG), 2026, to appear.