Advanced Examples

Everything that is done in the campaign can also be done in a mod - and this is a good way to get a feel for the modding API. This page walks through how to recreate some of the levels from the campaign that illustrate various common techniques. A convenient copy of the Campaign in mod form can be downloaded here and this is good to use as a reference to see how various levels are made and to copy these techniques for your own use.

Adding New Blocks

The simplest modification one might make to a level would be to add a new kind of block to it. This is ubiquitous - it can be done simply by enlarging the list of blocks in setup.lua.

blocks = {"Red.png", "Blue.png", "Green.png", "White.png"},

These files must either be in the game's global library (which should only be assumed to contain the blocks listed above) or must be in the level folder. For instance, the level "Zebra" in the campaign includes a file Piece.png and adds

blocks = {"Red.png", "Blue.png", "Piece.png"},

to the setup, loading that piece into the game. By default, it is assumed that orientation does not matter for added blocks; this is only true if the image has the same symmetries as a square. If not, a little bit of Lua needs to be specified, as it is in Zebra.

To specify symmetries of a block explicitly, we first add the following line to setup.lua

main = "level_logic.lua",

and then, in a new file called level_logic.lua, add the following:

settings.block_info[3] = {symmetries = {ID, R2, F1, F3}}

This code modifies the settings for the third entry in the blocks list in setup.lua (which is "Piece.png" in Zebra). It then lists the symmetries of the image - that is, all the ways you could transform the image without changing how it looks.

When correctly set up, checkers will now ensure that, in order for two blocks to be considered the same, their orientations must differ by a transformation that is on this list. Thus, in Zebra, a block rotated by 90 degrees will not be considered equal to the original block, nor will a block flipped vertically. However, a block rotated by 180 degrees will be considered the same. Be sure to test this carefully - it would be very frustrating to users if two orientations that look the same were not accepted (or two that didn't look the same were accepted). As a mathematical note, the only reasonable tables to set this field to are as follows1: {ID}, {ID, R2}, {ID, R1, R2, R3}, {ID, F0}, {ID, F1}, {ID, F2}, {ID, F3}, {ID, R2, F0, F2}, {ID, R2, F1, F3}, or {ID, R1, R2, R3, F0, F1, F2, F3}. If you think your object has a set of symmetries not listed here, you should think harder because you are not correct.

Randomizing Inputs and Outputs

A good example of randomized inputs and outputs is the level "Completor" from the campaign, which has both a randomized input and output. To create this level, one would start by creating a level as in the simple tutorial, creating two generators and one checker. A stripped down version of "Completor" is provided here; it is recommended to download this one, since the real "Completor" level features some significantly more complicated code to implement a tutorial.

Next, we need to store the data for the randomized inputs, since the game cannot simply copy the pieces into place. To do this, first add the key

extra_keys = true,

into the table passed to standard_setup. This informs the standard library that we want to store some extra data. Keep the in_creation_mode flag set as well.

Open the level again and create whatever shapes are needed. For "Completor", we would create the following shapes:

The shapes in Completor

Then, in the console, we would give the command

capture_keys_tool("triangle_in", "triangle_out", "square")

Which would then prompt us to select three region, one associated to each key. After doing this, a value extra.triangle_in will be accessible to Lua containing the information necessary to recreate the shape1. After doing this, you can more the start_save file into place and disable creation mode in setup.lua.

Next, we add the code to tell the game to use our newly saved shapes instead of just copying. The basic format for doing this is to create a file called level_logic.lua (or whatever you want to call it) and then add, in setup.lua, a flag main = "level_logic.lua", which tells the game to open your new script as part of the simulation.

In level_logic.lua, we can modify the generators and checkers. The basic format for doing this would be to write some code as follows:

location.std.generators[1].dynamic = function(ct)
  ...
end

This will modify the generator at index 1 (i.e. the first one we created). There are various other options that one can play with, but the .dynamic option of a generator is one of the more powerful tools to use.

The defined function takes one argument ct which starts at 1 and increases by 1 for each additional block we want to create. It should return whatever shape we want to generate. In Completor, we define a function is_square(ct) which takes an index and returns whether a square should be made or not - and can then fill in the function as:

location.std.generators[1].dynamic = function(ct)
  if is_square(ct) then
    return extra.square
  else
    return extra.triangle_in
  end
end

This will create a sequence of triangles and squares according to whether is_square(ct) returns true. In the code for Completor, is_square is just implemented by cycling through a table of values - theoretically, one could randomize its behavior, but it's wiser to ensure that the simulation remains deterministic and does the same thing every time, especially since this allows fine tuning the sequence if necessary. Implementing such a function is purely an exercise in Lua - there are lots of examples in the campaign to look at as well as plenty of places to learn Lua in general, so we won't dive deeply into this here.

For the checker, we write a similar function

location.std.checkers[1].dynamic = function(ct)
  if is_square(ct) then
    return extra.square
  else
    return extra.triangle_out
  end
end

which specifies the sequence of outputs. Note that it's important to get the order coordinated between the generator and the checker - the level would not work otherwise.


  1. Specifically, extra.triangle_in will be a list of interactions that would recreate the object at (0, 0) if executed. Most standard library functions expect this kind of list as an input.