Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

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

Comments

  • dave1707dave1707 Mod
    Posts: 10,053

    @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.

  • dave1707dave1707 Mod
    edited January 2016 Posts: 10,053

    @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.

  • Posts: 12

    @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.

  • dave1707dave1707 Mod
    Posts: 10,053

    @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.

  • Posts: 12

    @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 ;-)

    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!

  • dave1707dave1707 Mod
    Posts: 10,053

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

  • AnatolyAnatoly Mod
    Posts: 894

    @quiath, some are doubled some not existing

  • Posts: 12

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

  • dave1707dave1707 Mod
    Posts: 10,053

    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.

  • Posts: 12

    @dave1707 Thank you, that makes sense. ;-)

  • Posts: 509

    @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,"\n"))
        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
    
  • Posts: 12

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

  • Posts: 509

    @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.

Sign In or Register to comment.