Patterns - Wolfram's elementary cellular automaton explorer

In the New Year 2016 I feel I could make a tiny contribution to the community. This is on a topic of cellular automatons although this particular flavor hasn’t been discussed in these forums so far.

I’m presenting a pretty small Wolfram’s elementary cellular automaton explorer. Additional information:
http://mathworld.wolfram.com/ElementaryCellularAutomaton.html

You can “program” it by setting if the particular pattern of the three cells above should make the current cell “on” or “off”. There are 256 possible patterns - or rules as they call them - and I made the numbering compatible with Wolfram. Double tap changes the starting conditions - single “on” cell / single “off” cell / random cells, there is an independent setting if the sides wrap. Dragging the image works as expected.

This project is not very advanced so I hope someone learning to program might find something valuable for them. And anyway the patterns are quite interesting in their own right.

Additional notes.

I constrained myself to using just one tab and no classes, getting under 200 lines. There are four global variables which in any serious application should be upgraded to classes. The cell values are stored in the memory and later drawn to screen each frame, which is why the performance is not great. More effective, but also a bit more complicated, is generating the cells row by row and drawing them to a buffer using setContext() - which I implemented as another project. Even better would be to generate the cells using GPU which I saved for the future.

Enjoy!

-- Patterns

-- Use this function to perform your initial setup
function setup()
    -- contains variables concerning drawing the cells 
    graph = { 
        block = readImage("Planet Cute:Icon"),
        scalef = 0.125,
        offx = 0,
        offy = 0
    }
    -- 2 dimensional table holding the actual cell contents
    local N = 120
    arr = {}
    for y = 1,N do
        arr[y]={}
        for x = 1,N do
            arr[y][x]=0
        end
    end
    -- parameters
    params={wrap=true, def=0, 
        pat={0,1,1,1,1,1,1,0},
        rule=-1, -- will be recomputed
        reinitmode=0
    }
    reinit(arr, params)
    -- UI stuff
    ui={
        uiscale = 0.4,
        uipitch = 1.1,
        uipos = {},
        drag = false,
        block = readImage("Planet Cute:Plain Block"),
        dt = 1/60
    }
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(40, 40, 50)

    -- Do your drawing here
    
    for y = 1,#arr do
        for x = 1,#arr[y] do
            if arr[y][x]>0 then
                tint(31, 97, 41, 255)
            else
                tint(159, 215, 12, 255)
            end
            
            sprite(graph.block,
                graph.block.width * graph.scalef * x + graph.offx,
                HEIGHT - graph.block.height * graph.scalef*y + graph.offy,
                graph.block.width * graph.scalef,
                graph.block.height * graph.scalef)
        end
    end  
    -- UI
    local sc=ui.uiscale/3
    local tints = {[0]=color(255, 255, 255, 214),   [1]=color(255, 0, 0, 204)}
    for i=1,8 do
        local k = i - 1 -- 0 based
        local b = { ((k & 4) > 0) and 1 or 0,
                    ((k & 2) > 0) and 1 or 0,
                    ((k & 1) > 0) and 1 or 0 }
        local iw = 9 - i -- wolfram compatible
        tint(tints[params.pat[i]])
        local ux, uy = ui.block.width*ui.uiscale*ui.uipitch*iw, 
                        HEIGHT-ui.block.width*ui.uiscale*2
        local uw, uh = ui.block.width*ui.uiscale, ui.block.height*ui.uiscale
        sprite(ui.block, ux, uy, uw, uh)
        
        ui.uipos[i] = {x=ux-uw/2,y=uy-uh/2,w=uw,h=uh}
        for j=1,3 do
            tint(tints[b[j]])
            sprite(ui.block,
                ux+(ui.block.width*sc*(j-2)),
                HEIGHT-ui.block.width*ui.uiscale*1.5,
                ui.block.width*sc,ui.block.height*sc)
        end   
    end
    
    tint(255, 255, 255, 255)
    fill(255, 255, 255, 255)
    local t={"101011", "0001000", "1110111"}
    ui.dt = 0.9 * ui.dt + 0.1 * DeltaTime
    text("#" .. params.rule .. "  " .. 
         t[params.reinitmode%3+1] .. 
         (params.reinitmode<=3 and "<->" or "") ..
         string.format(" %4.1f FPS", 1/ui.dt),
         100, HEIGHT-10)
end

function getthree(row, x, data)
    local v = { data.def, data.def, data.def }
    for i=-1,1 do
        if x+i < 1 or x+i > #row then
            if data.wrap then
                v[i+2]=row[(x+i-1+#row)%#row+1]
            end
        else
            v[i+2]=row[x+i]
        end
    end
    return 1+4*v[1]+2*v[2]+1*v[3]
end

function ruleno(data)
    local ptwo,sum=1,0
    for i=1,8 do
        sum = sum + data.pat[i]*ptwo
        ptwo = ptwo * 2
    end
    return sum    
end

function wolfram(a, data)

    data.rule=ruleno(data)    
    
    for y = 2,#a do
        for x = 1,#a[y] do
            local idx=getthree(a[y-1],x,data)
            a[y][x]=data.pat[idx]
        end
    end
    return a
end

function touched(touch)
    local x,y=touch.x, touch.y
    if touch.state == BEGAN then
        for i = 1, #ui.uipos do
            local r = ui.uipos[i]
            if x >= r.x and y >= r.y and
               x-r.x <r.w and y-r.y < r.h 
            then
                params.pat[i]=1-params.pat[i]
                wolfram(arr, params)
                return
            end
        end
        
        if touch.tapCount > 1 then
            reinit(arr, params)
        end
        
        ui.drag = true
    elseif touch.state == MOVING then
        if ui.drag then
            graph.offx = graph.offx + touch.deltaX
            graph.offy = graph.offy + touch.deltaY
        end
    elseif touch.state == ENDED then
        if ui.drag then
            graph.offx = graph.offx + touch.deltaX
            graph.offy = graph.offy + touch.deltaY
        end
        ui.drag = false
    end
end

function reinit(arr, data)
    data.reinitmode = data.reinitmode == 6 and 1 or data.reinitmode + 1
    data.wrap = data.reinitmode <= 3

    if data.reinitmode % 3 == 0 then
        for i=1,#arr[1] do
            arr[1][i]=math.random(0,1)
        end
    else
        local v = (data.reinitmode % 3) == 1 and 0 or 1
        for i=1,#arr[1] do
            arr[1][i]=v
        end
        
        arr[1][#arr[1]//2]=1-arr[1][#arr[1]//2]         
    end

    wolfram(arr, data) 
end

@guiath Very interesting. I’m not sure how it works, but I’ll read up on the info you provided. I find this type of code more interesting than games.

@quiath I looked at the link you provided and it’s kind of interesting. I have a question because I may not be understanding what exactly is supposed to happen. Looking at the link you provided where they show the 8 rules at the top of the page, rule 1 (right most) shows that if 3 squares in a row are off, then the center square of those 3 in the next generation will be off. If that’s the case, then no squares for rule 1 should be turned on, yet you’re program and the link example shows squares on. Apparently I’m missing something.

@dave1707 In my program the first row is not computed, but it’s seeded in the following way:

  1. a single “on” cell, double tap for:
  2. a single “off” cell in a row of “on” cells, double tap for:
  3. a randomly generated first row, double tap for:
    4-6. repeat of the above with left-right wrapping turned on.

Simply speaking, you need some seed, even very simple, for the patterns to be interesting.

@quiath I understand the rules for your program and the need for the starting single seed. But for rule 1 (right most) in the link, it shows that if 3 squares in a row are off, then the center square of those 3 squares in the next generation will be off. If rule 1 doesn’t turn any squares on, then nothing should change from generation to generation. But in the link example and your program, there are squares turned on. Shouldn’t the seed be the only square turned on when rule 1 is run.

@dave1707 I think it’s best if I show it on an example. Let’s use the rule shown at the top of the page I linked earlier:
http://mathworld.wolfram.com/images/eps-gif/ElementaryCA30Rules_750.gif

(rule 30)
Let’s start with an initial row containing 7 columns marked by letters. There are infinitely many columns to the left and right, we assume all of them contain 0:

| a | b | c | d | e | f | g |
-----------------------------
| 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1st

In the second row we will assign the values in the following way, looking at the existing values in the row immediately above, i.e. the first row:
For column a: look up 000 → result 0
For column b: look up 000 → result 0
For column c: look up 001 → result 1
For column d: look up 010 → result 1
For column e: look up 100 → result 1
For column f: look up 000 → result 0
For column g: look up 000 → result 0

The result is this second row:

| a | b | c | d | e | f | g |
-----------------------------
| 0 | 0 | 1 | 1 | 1 | 0 | 0 | 2nd

From here we iterate the rows up to infinity :wink:

For the third row:

For column a: look up 000 → result 0
For column b: look up 001 → result 1
For column c: look up 011 → result 1
For column d: look up 111 → result 0
For column e: look up 110 → result 0
For column f: look up 100 → result 1
For column g: look up 000 → result 0

In the result we receive a growing triangle pattern:

| a | b | c | d | e | f | g |
-----------------------------
| 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1st
| 0 | 0 | 1 | 1 | 1 | 0 | 0 | 2nd
| 0 | 1 | 1 | 0 | 0 | 1 | 0 | 3rd
...

And so on.

Hope that helps!

@quiath I understand it now. I was totally misinterpreting the 8 rules. Sorry for the trouble.

@quiath, some are doubled some not existing

@TokOut What is doubled/not existing, would you care to elaborate?

I think he means that some values show the same pattern and some other values don’t draw a pattern at all. He thinks that every value should show a different pattern.

@dave1707 Thank you, that makes sense. :wink:

@quiath Here’s a shader version. I like your UI, but for simplicity I went with Codea’s parameters in this (so rules are specified by their number rather than by choosing the individual actions). I count 193 lines, so still under your self-imposed limit of 200.

-- CellularShader

function setup()
    cellular = mesh()
    cellular:addRect(.5,.5,1,1)
    width = 10
    rule = 18
    setShader()
    backingMode(RETAINED)
    parameter.integer("rule",0,255,18,setRule)
    parameter.integer("width",10,2*WIDTH,10,setWidth)
    parameter.boolean("wrap",setShader)
    parameter.boolean("continuous", function(b)
        if b then
            step = true
        end
    end)
    parameter.action("step",function() step = true end)
    parameter.action("restart",clearImgs)
    parameter.watch("math.floor(1/DeltaTime)")
    noSmooth()
    step = true
end

function draw()
    if step then
        doRule()
        step = continuous
    end
end

function doRule()
    pushMatrix()
    translate(WIDTH/2,y)
    scale(WIDTH/width)
    sprite(imga)
    popMatrix()
    y=y-sf
    if y< 0 then
        y = HEIGHT-sf/2
        background(0, 0, 0, 255)
        pushMatrix()
        translate(WIDTH/2,y)
        scale(WIDTH/width)
        sprite(imga)
        popMatrix()
    end
    setContext(imgb)
    background(0, 0, 0, 255)
    cellular:draw()
    setContext()
    imga,imgb = imgb,imga
    cellular.texture = imga
end

local patterns = {
    "OOO",
    "OOX",
    "OXO",
    "OXX",
    "XOO",
    "XOX",
    "XXO",
    "XXX"
}

local result = {"O", "X"}

function setRule(r)
    local rules = {}
    output.clear()
    local disp = {"Rule:"}
    for k=1,8 do
        table.insert(rules,r%2)
        table.insert(disp,patterns[k] .. " -> " .. result[r%2 + 1])
        r = r//2
    end
    print(table.concat(disp,"\
"))
    cellular.shader.rules = rules
    clearImgs()
end

function setShader(b)
    cellular.shader = automata(b)
    setWidth(width)
    setRule(rule)
end

function setWidth(w)
    width = w
    sf = WIDTH/w
    y=HEIGHT-sf/2
    cellular:setRect(1,width/2,.5,width,1)
    imga = image(width,1)
    imgb = image(width,1)
    cellular.texture = imga
    cellular.shader.width = width
    clearImgs()
end

function clearImgs()
    setContext(imga)
    background(0, 0, 0, 255)
    setContext()
    imga:set(math.floor(width/2),1,color(255,255,255,255))
    background(0, 0, 0, 255)
    y = HEIGHT-sf/2
    step = true
end

function automata(b)
    local wrap
    if b then
        wrap = function(s)
            return "fract(" .. s .. ")"
        end
    else
        wrap = function(s)
            return s
        end
    end
    return shader(
    [[
//
// A basic vertex shader
//

//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;

//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

//This is an output variable that will be passed to the fragment shader
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;

void main()
{
    //Pass the mesh color to the fragment shader
    vColor = color;
    vTexCoord = texCoord;
    
    //Multiply the vertex position by our combined transform
    gl_Position = modelViewProjection * position;
}

    ]],[[
//
// A basic fragment shader
//

//Default precision qualifier
precision highp float;

//This represents the current texture on the mesh
uniform lowp sampler2D texture;

uniform int rules[8];
uniform float width;
float rw = 1./width;
//The interpolated vertex color for this fragment
varying lowp vec4 vColor;

//The interpolated texture coordinate for this fragment
varying highp vec2 vTexCoord;

int parents(float x)
{
    int v = 0;
    v += int( texture2D(texture,vec2(]] .. wrap("x-rw") .. [[,.5)).r);
    v += 2*int( texture2D(texture,vec2(x,.5)).r);
    v += 4*int( texture2D(texture,vec2(]] .. wrap("x+rw") .. [[,.5)).r);
    return v;
}

void main()
{
    //Sample the texture at the interpolated coordinate
    int i = parents(vTexCoord.x);
    vec4 col = vec4(0.);
    col.r = float( rules[i]);
    //Set the output color to the texture color
    gl_FragColor = col;
}

    ]]
    )
end

@LoopSpace Very interesting since the continuous button lets you stare at the patterns until infinity :wink:
BTW does it compute one row per frame?

@quiath Yes, in continuous mode then it is one row per frame. One could do more per frame without affecting the framerate, I guess, as it’s quite cheap to do a row on the gpu.