Skip to content

TodeSplat Tutorial

Luke Wilson edited this page Nov 11, 2019 · 23 revisions

TodeSplat is a language that helps you make elements for the Sandboys engine.
It is heavily inspired by SPLAT and shares many principles.


The most important part of TodeSplat is the element expression. It defines a new element:

element Sand {}


You can give your element properties. Space them out on different lines:

element Sand {
    colour "yellow"
    meaningOfLife 42

Some properties are in-built and affect things about the element, like colour.
These are all the in-built properties:

  • colour
  • emissive (emissive colour of the element)
  • opacity
  • precise (if true, the dropper tool will only place one atom of the element at a time)
  • floor (if true, the dropper tool will place the atom on the floor instead of dropping it from the air)
  • hidden (if true, the element will not appear in the menu
  • category (determines which menu category to appear in)
  • continuous (not properly implemented yet, if true, holding down the mouse button will continue to pour more atoms, default is true)


Atoms of the element can carry data.
Use the data keyword to shows what data the atom holds, and its default value.

element Sand {
    colour "yellow"
    data isWet false
    data temperature 15


You can give your element rules by using the rule keyword:

element Sand {
    colour "yellow"
    rule {
        @ => _
        _    @

    rule {
        @ => _
        x    x


An element's rules show it how to act in the Sandboys world.
It checks its surroundings to see if it matches the left-hand-side of the rule (the inputs).
If it matches, it changes it to look like the right-hand-side of the rule (the outputs).
Rules are checked in order. The first rule to match is the one that gets used.


A rule's diagram shows how things are arranged.
Let's look at a rule more closely:

rule {
    @ => _
    _    @

The @ symbol shows where the element is.
The _ symbol checks for an empty space.
In other words, the rule checks if there is an empty space below the element.

The @ symbol shows where to put the element.
The _ symbol shows where to put an empty space.
In other words, the rule moves the element down, leaving an empty space behind.


Inputs are the characters you write on the left-hand-side of a rule diagram to check an element's surroundings.
There are some in-built inputs that you can use straight away:

  • @ This atom.
  • _ An empty space.
  • # A non-empty space.
  • . Any space.
  • x Not a space (ie: the edge of the universe).
  • * Anything.

You can also define your own inputs.
You do this by defining a Javascript function that checks the space.
You can define an input within an element to make it only available to that element.
Or you can define in outside any element to make it globally available to all elements.

input S ({space}) => space && space.atom && space.atom.element == Sand

In the above input, you can see that that Javascript function checks that (1) the space exists, (2) it contains an atom, and (3) the atom is sand.
You can also do multi-line functions by ending a line with { and ending the function with } at the same indentation as the start:

input S ({space}) => {
     if (!space) return false
     if (!space.atom) return false
     return space.atom.element == Sand


Outputs are almost exactly the same as inputs. Instead of checking the space, they define the instruction for how to change the space.
Some are built-in already:

  • @ Place this atom in the space.
  • _ Empty the space.
  • . Do nothing.

You can make your own outputs in the same way as inputs.
The SPACE and ATOM objects can help:

output S ({space}) => {
     const atom = ATOM.make(Sand)
     SPACE.setAtom(space, atom)

The above output makes a new Sand atom and puts it in the space.


You can add a reflection label to a rule to make it randomly choose a way of reflecting itself.

rule y {
    @ => _
    _    @

The above rule has a y reflection. It will randomly reflect itself in the y-axis.
In other words, it might fall down, or it might fall up.

The reflections label can be any combination of x, y and z.
eg: xz
eg: xyz


You can add a symmetry label to make a rule always symmetrical in that axis.
You write it just like reflections but with capital letters instead.

rule Y {
   @ => @
   .    _

The above rule will empty the space above and below an atom.


You can add a number label to say the chance of a rule happening:

rule 0.5 {
    @ => _
    _    @

The above rule only happens 50% of the time.

Point of View

By default, the rule diagram is positioned as if you are looking from the front.
You can give the rule a label to make the diagram represent another point of view.

rule top {
    @ => _
    _    @

The above rule's diagram is shown from a bird's eye view instead of from the front.
Valid points of view:

  • front (default)
  • top
  • side


You can transfer data from one diagram layer to the next.
You can do this by adding a property to the args parameter:

input S ({space, args}) => {
    if (space && space.atom && space.atom.element == Sand) {
        args.sand = space.atom
        return true

Then, you can access the argument in an output:

output S ({space, sand}) => {
    // Place the sand atom into the space
    SPACE.setAtom(space, sand)

Input Layers

Rules can have more than one layer of inputs.

input a ({self}) => self.isAwake
rule xyz { @a => @_ => _@ }

The above rule, (1) checks if the atom is awake, (2) checks if the next space is empty, (3) moves into it.


Use the action keyword to give actions to an element.
Elements always try to do every action. This is different to rules: Elements only do the first rule that matches.

element Dropper {
    output S ({space}) => SPACE.setAtom(space, ATOM.make(Sand))
    // Drop some sand below me
    action {
        @ => @
        _    S
    // Afterwards... move into space
    action xyz { @_ => _@ }

Multi-Layer Matching

These in-built inputs can be useful when working with symmetries:

  • ?: Checks that success was set to true.
  • !: Checks that success was not set to false.
input f ({space, args}) => {
    if (space && space.atom && space.atom.element == Fire) args.success = true
    return true

output F ({space}) => SPACE.setAtom(space, ATOM.make(Fire))

rule XYZ { @f => ?? => FF }
  • @f First Input: If there is Fire in any space around me, mark success as true.
  • ?? Second Input: Check that success was marked as true.
  • FF Output: Make fire everywhere around me.

In other words, if there is a Fire atom next to me, explode into flames.

Tally (not properly implemented yet)

Use the in-built ^ input to count up successful inputs.
This example is similar to the previous one:

input f ({space, args}) => {
    if (args.threshold == undefined) {
        args.tally = 0
        args.threshold = 2
    if (space && space.atom && space.atom.element == Fire) args.tally++
    return true

output F ({space}) => SPACE.setAtom(space, ATOM.make(Fire))

rule XYZ { @f => ^^ => FF }
  • @f First Input: If there is Fire in any space around me, increase the tally.
  • ^^ Second Input: Check that the tally meets the minimum threshold.
  • FF Output: Make fire everywhere around me.

In other words, if there are at least two Fire atoms next to me, explode into flames.


Inputs can extend other inputs to combine them together.

input S extends # ({space}) => space.atom.element == Sand

This extends the in-built # input that checks that there is an atom in the space.


Elements can copy another element's rules (and actions).

element Snow {
    colour "yellow"
    output W ({space}) => SPACE.setAtom(space, ATOM.make(Water))
    rule 0.01 { @ => W }
    ruleset Sand

This snow element melts 1% of the time. The rest of the time, it behaves like Sand.

Clone this wiki locally
You can’t perform that action at this time.