ASP level generation examples from our PCG textbook

Now updated to clingo 4/5 and interactive on the web

This is an updated, interactive web version of all but one of the answer-set programming (ASP) level-generation examples in Chapter 8 of our Procedural Content Generation textbook:

The examples in that chapter were all written in the syntax of clingo 3, which changed significantly with the release of clingo 4 (but has fortunately stayed the same in clingo 5, which is current as of 2025). All but one of them were straightforward to port, which I've done here. The existence of clingo-wasm also makes it possible to make interactive web demos.

For a full tutorial, please see the book chapter. This page just has the examples with a brief description of each one. For each example, there are three tabs. From right to left: the ASP code for the example; an answer set produced by running the solver (press "Solve" again to get a different one); and a visual rendering of that answer set as a level. You can edit the ASP code on the code tab if you'd like.

Mazes

The first three examples generate mazes.

Mazes 1: Core definition

A common representation of a maze, borrowed from the maze-generation literature, is a tree. We have an n×n grid of maze cells, and each one has exactly one parent cell, which must be one of its four neighbors. Cells are visualized as connected if and only if they have a parent/child relationship.

This corresponds to the code from Fig. 8.1 (maze-core.lp).

Hint: Try changing width to 10 or 50 to get larger mazes.

Click the "Solve" button to run the solver.

Mazes 2: Require connectivity

The above code does not guarantee that all cells in the maze are part of the same tree, so they do not necessarily form what we normally think of as a maze. We can add three lines of ASP code to the previous code, which adds a requirement to be fully connected. This corresponds to adding the code from Fig. 8.3 (maze-reach.lp) to the code from the previous example.

Side note: this illustrates ASP's elaboration tolerance: When things go right, more requirements can be added by simply appending more lines of code, without editing the previous code.

Click the "Solve" button to run the solver.

Mazes 3: Add style constraints

Next we will add a style constraint: we will arbitrarily decide that we want very "horizontal" mazes, with long left-to-right corridors and not many vertical connections. This is a soft constraint, meaning that code from the previous two examples takes priority: it's more important for the maze to fit the basic definition, and to be fully connected, than it is for it to be horizontal. But, all else being equal, we do prefer more horizontal mazes. In this case, we implement the soft constraint using a #minimize rule.

This corresponds to adding the code from Fig. 8.5 (maze-bias.lp) to the code from the previous two examples.

Technical note: This straightforward implementation is not efficient. It works fine at the default 5×5, but will grind to a halt if you increase the maze size much above that. In the interests of keeping the code as close to the book as possible, I will resist the urge to optimize it for now.

Click the "Solve" button to run the solver.

Dungeon-crawler levels

The next three examples generate levels for a dungeon-crawler game. The player enters at the top-left corner, and to clear the level, they need to bring a gem (G) to an altar (A) before exiting at the bottom-right corner.

Levels 1: Core definition

A level is again an n×n grid. Two cells are said to be adjacent if they touch each other horizontally or vertically (we don't yet use this fact, but will use it for typical grid-world movement later). Every grid cell has at most one of the three sprites: wall, gem, or altar (it may also be empty). The altar and gem are limited to one per level each (the number of walls is not limited).

This corresponds to the code from Fig. 8.7 (level-core.lp).

Technical note: The examples you generate below will probably never have any walls in them, even though there are answer sets with walls. I randomize the solving in this demo, but clingo is not really designed for random sampling, so the distribution can sometimes be strange (and can vary between versions of the solver). Two better solutions if you want more random sampling: 1) generate a lot of levels and just choose among them randomly (generating a lot of levels is fast), or 2) use xorro, which is designed to sample answer sets. Or, move on to the next example that adds style constraints.

Click the "Solve" button to run the solver.

Levels 2: Add style constraints

Next we will add some style constraints. Unlike with the maze style constraints, these will be "hard" constraints, meaning that all generated levels must follow them.

This corresponds to adding the code from Fig. 8.9 (level-style.lp) to the code from the previous example. Again, the new style-constraint code is simply concatenated to the existing core level code.

Click the "Solve" button to run the solver.

Levels 3: Add simulation constraints

So far we have added some constraints about the layout, but have not yet guaranteed that the level is playable. Recall that we described gameplay as follows: "The player enters at the top-left corner, and to clear the level, they need to bring a gem (G) to an altar (A) before exiting at the bottom-right corner."

Therefore there are three reachability requirements: to get from the entrance to the gem, from the gem to the altar, and from the altar to the exit. We will implement this as a little state-machine simulation.

This corresponds to adding the code from Fig. 8.11 (level-sim.lp) to the code from the previous two examples.

Click the "Solve" button to run the solver.

Missing: No-shortcuts constraint

There is one final example in the book chapter, in Section 8.6. Our simulation constraint above added a constraint that levels must be playable. But once we have that, a next step could be to add style constraints on the playability. For example, we may want the level to be playable, but not too easily playable: The minimum path through the level cannot be shorter than some number of steps. Unfortunately, this is not directly solvable in base ASP. It requires what my coauthor on this chapter, Adam Smith, calls quantifying over play. The version presented in our book chapter (which was adapted from his FDG 2013 paper) required a meta-programming pipeline that does not directly port to current clingo-wasm, so it is omitted here.