Tozier and I got together Tuesday, and decided to play a bit more with the spiders and ants. We’re interested in it because it’s kind of fun watching them run around, because we wanted to get insight into the mathematical question of whether the spiders can catch the ant, and because Tozier will surely work a Genetic Programming exercise on this sooner or later.

We were concerned about the way the ant was deciding what to do. It seemed to turn onto lines with a spider when it shouldn’t, and we couldn’t figure out whether it was happening, nor why it was, if it was. So we thought of the idea of “hot corners”. We wanted to identify visually a corner that had a spider “close” to it. The idea of “close” is that the spider could get to the corner in the time it would take the ant to traverse a whole edge. For example, if the ratio of ant speed to spider speed is 3, then a corner should be “hot” if a spider is within 1/3 of the edge length. We wanted to highlight those corners as we observed what decisions the spiders and ant might make. So we built this:

isHotCorner  = { false, false, false, false, false, false, false, false }

function drawCubes()
    -- deleted for clarity 
    progress()
end

function progress()
    isHotCorner  = { false, false, false, false, false, false, false, false }
    idHotCorners(s1)
    idHotCorners(s2)
    idHotCorners(s3)
    drawCorners()
    if ( not dead ) then
        moveAnt(ant)
        moveBug(s1)
        moveBug(s2)
        moveBug(s3)
    end
    dead = isAntDead()
    drawBug(s1, color(255,0,0))
    drawBug(s2, color(255,0,0))
    drawBug(s3, color(255,0,0))
    drawBug(ant, color(0,255,0))
    --drawSpiderLines()
end

function idHotCorners(bug)
    local oneStep = s1.speed/ant.speed
    if bug.frac < oneStep then isHotCorner[bug.start] = true end
    if 1-bug.frac < oneStep then isHotCorner[bug.stop] = true end
end

function drawCorners()
    for corner,bool in ipairs(isHotCorner) do
        if bool then drawBox(corner) end
    end
end

The result of this is that as a spider moves toward a corner, it gets a box drawn around it, and as he moves away, after a while the box goes away. It looks like this:

Sure enough we found that the spider was making bad decisions sometimes. We found this bug:

-- WAS
function nextAntGoal(index)
    local choice
    local choices = shuffle(cube[index])
    --print("choices " .. choices[1] .. ", " .. choices[2] .. ", " .. choices[3])
    local try = 1
    while try < 3 do
        choice = choices[try]
        if safeEdge(index, choice) then 
            --print("safe " .. choice)
            return choice 
        end
        try = try + 1
    end
    print("forced ".. choice)
    return choice
end

--SHOULD BE
function nextAntGoal(index)
    local choice
    local choices = shuffle(cube[index])
    --print("choices " .. choices[1] .. ", " .. choices[2] .. ", " .. choices[3])
    local try = 1
    while try <= 3 do
        choice = choices[try]
        if safeEdge(index, choice) then
            return choice 
        end
        try = try + 1
    end
    print("forced ".. choice)
    return choice
end

What was happening was that the while said < 3 and should have said <= 3. This meant that the ant would never select the third path, even if the second one was unsafe. He just accepted his fate, which was usually bad.

With the change above, the ant was making better decisions. They still weren’t great. For example, suppose the ant proceeds down a safe edge, but the corner at the other end is hot: that is, a spider can get there before the ant does. This is bad: if everyone starts at zero (and at this point they were), the ant will run right into his demise. So we decided to give the ant more intelligence: if you see the corner ahead of you go hot, reverse. It goes like this:

function moveAnt(ant)
    if isHot(ant.stop) then reverseAnt(ant) end
    moveBug(ant)
end

function isHot(corner)
    return isHotCorner[corner]
end

Note that this code is a bit odd. We wrote moveAnt by intention, inquiring whether the ant’s stop location was hot, thinking it might be tricky. But it wasn’t so we just implemented isHot. We should have folded that function back in, but we didn’t.

And thereby hangs a tale, which we’ll talk about next time.

Seems messy …

As we built this thing, I tended to type in new functions at locations where I could see the things that related to them, not in any particularly organized fashion. As we worked, we noticed more and more that we were looking up and down the file to find functions.

We also noticed that having three spider definitions and one ant written out longhand was a problem:

ant = {}
ant.start = 1
ant.stop = 2
ant.frac = math.random()
ant.speed = speed*3
ant.nextGoal = nextAntGoal

s1 = {}
s1.start = 2
s1.stop = 6
s1.frac = math.random()
s1.frac = 0
s1.speed = speed
s1.nextGoal = nextGoal

s2 = {}
s2.start = 7
s2.stop = 3
s2.frac = math.random()
s2.frac = 0
s2.speed = speed
s2.nextGoal = nextGoal

s3 = {}
s3.start = 4
s3.stop = 1
s3.frac = math.random()
s3.frac = 0
s3.speed = speed
s3.nextGoal = nextGoal

Every time we wanted to change where they started or anything like that, we had to change all four of these little bits of code. Since we wanted to observe various things, we did that a lot. It was a pain.

We needed some kind of object to represent our bugs. If not an actual class, then at least some kind of copyable table. We went through a phase like that in the Spacewar game and it might have been enough for this situation, though my inclination is to go directly to a class (or even a little hierarchy). I don’t know what we’d do until we do it, but I can speculate.

As you read the code below, make your own assessment of its quality. I think you’ll agree that it isn’t very good.

Then tune in soon for the next article, which accidentally, but conveniently, makes a point I feel needs to be made often. See you then!

-- Ant and Spiders

corners = { 
    vec2(-400, -400), vec2(-400,400 ), vec2(400, 400), vec2(400, -400),
    vec2(-200, -200), vec2(-200,200 ), vec2(200, 200), vec2(200, -200)
}

cube = { {2,4,5}, {1,3,6}, {2,4,7}, {1,3,8}, {1,6,8}, {2,5,7}, {3,6,8}, {4,5,7} }

isHotCorner  = { false, false, false, false, false, false, false, false }

local speed = 0.005
local dead = false

function nextGoal(index)
    local choice = math.random(3)
    local choices = cube[index]
    return choices[choice]
end

function nextAntGoal(index)
    local choice
    local choices = shuffle(cube[index])
    --print("choices " .. choices[1] .. ", " .. choices[2] .. ", " .. choices[3])
    local try = 1
    while try <= 3 do
        choice = choices[try]
        if safeEdge(index, choice) then
            return choice 
        end
        try = try + 1
    end
    print("forced ".. choice)
    return choice
end

function shuffle(tab)
    local dex = {1,2,3}
    local res = {}
    local next
    next = math.random(1,3)
    table.insert(res, tab[dex[next]])
    table.remove(dex, next)
    next = math.random(1,2)
    table.insert(res, tab[dex[next]])
    table.remove(dex, next)
    table.insert(res, tab[dex[1]])
    return res
end

function safeEdge(start, stop)
    return not spiderOn(start,stop)
end

function spiderOn(start, stop)
    return thisSpiderOn(s1, start, stop) or thisSpiderOn(s2, start, stop) or thisSpiderOn(s3,start,stop)
end

function thisSpiderOn(spider, start, stop)
    return (spider.start == start and spider.stop == stop) or (spider.stop == start and spider.start == stop)
end

ant = {}
ant.start = 1
ant.stop = 2
ant.frac = math.random()
ant.speed = speed*3
ant.nextGoal = nextAntGoal

s1 = {}
s1.start = 2
s1.stop = 6
s1.frac = math.random()
s1.frac = 0
s1.speed = speed
s1.nextGoal = nextGoal

s2 = {}
s2.start = 7
s2.stop = 3
s2.frac = math.random()
s2.frac = 0
s2.speed = speed
s2.nextGoal = nextGoal

s3 = {}
s3.start = 4
s3.stop = 1
s3.frac = math.random()
s3.frac = 0
s3.speed = speed
s3.nextGoal = nextGoal

function setup()
    center = vec2(WIDTH/2, HEIGHT/2)
end

function draw()
    background(140, 140, 150)
    strokeWidth(5)
    drawCubes()
end

function drawCubes()
    rectMode(CENTER)
    fill(0,0,0,0)
    lineCapMode(SQUARE)
    rect(center.x, center.y, 400, 400)
    rect(center.x, center.y, 800, 800)
    translate(center:unpack())
    line(200,200, 400, 400)
    line(-200,-200, -400, -400)
    line(-200,200, -400, 400)
    line(200,-200, 400, -400)
    progress()
end

function progress()
    isHotCorner  = { false, false, false, false, false, false, false, false }
    idHotCorners(s1)
    idHotCorners(s2)
    idHotCorners(s3)
    drawCorners()
    if ( not dead ) then
        moveAnt(ant)
        moveBug(s1)
        moveBug(s2)
        moveBug(s3)
    end
    dead = isAntDead()
    drawBug(s1, color(255,0,0))
    drawBug(s2, color(255,0,0))
    drawBug(s3, color(255,0,0))
    drawBug(ant, color(0,255,0))
    --drawSpiderLines()
end

function idHotCorners(bug)
    local oneStep = s1.speed/ant.speed
    if bug.frac < oneStep then isHotCorner[bug.start] = true end
    if 1-bug.frac < oneStep then isHotCorner[bug.stop] = true end
end

function drawCorners()
    for corner,bool in ipairs(isHotCorner) do
        if bool then drawBox(corner) end
    end
end

function drawBox(corner)
    pushMatrix()
    translate(corners[corner].x, corners[corner].y)
    rect(0,0,20)
    popMatrix()
end

function drawSpiderLines()
    pushStyle()
    stroke(255,0,0)
    vline(corners[s1.start], corners[s1.stop])
    vline(corners[s2.start], corners[s2.stop])
    vline(corners[s3.start], corners[s3.stop])
    popStyle()
end

function vline(p,q)
    line(p.x, p.y, q.x, q.y)
end

function isAntDead()
    local radius = 10
    local pos = ant.pos
    if pos:dist(s1.pos) < radius then return true end
    if pos:dist(s2.pos) < radius then return true end
    if pos:dist(s3.pos) < radius then return true end
    return false
end

function moveAnt(ant)
    if isHot(ant.stop) then reverseAnt(ant) end
    moveBug(ant)
end

function isHot(corner)
    return isHotCorner[corner]
end

function reverseAnt()
    ant.start, ant.stop = ant.stop, ant.start
    ant.frac = 1 - ant.frac
end

function moveBug(aBug)
    aBug.frac = aBug.frac + aBug.speed
    if aBug.frac > 1 then
        aBug.start = aBug.stop
        aBug.stop = aBug.nextGoal(aBug.start)
        aBug.frac = 0
    end
    aBug.pos = (1-aBug.frac)*corners[aBug.start] + aBug.frac*corners[aBug.stop]
end

function nextGoal(index)
    local choice = math.random(3)
    local choices = cube[index]
    return choices[choice]
end

function drawBug(aBug, col)
    pushMatrix()
    pushStyle()
    translate(aBug.pos.x, aBug.pos.y)
    stroke(col)
    fill(col)
    ellipse(0,0,20,20)
    popStyle()
    popMatrix()
end

function bline(x1, y1,x2, y2)
    pushMatrix()
    pushStyle()
    fill(0,0,0)
    translate(x1, y1)
    local t = x1 .. " " .. y1
    text(t, 0, 0)
    translate(-x1, -y1)
    translate(x2, y2)
    t = x2 .. " " .. y2
    text(t, 0,0)
    popStyle()
    popMatrix()
    
    line(x1, y1, x2, y2)
end