A New Improved Ant
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