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
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 .. ", " .. choices .. ", " .. choices)
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 .. ", " .. choices .. ", " .. choices)
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

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 .. ", " .. choices .. ", " .. choices)
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])
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
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

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``````