My previous two commits say “Jitter is now probability based” and “Improve behaveJitter formatting”. Not much exciting there.

Before we look at the code, let me tell you some of the things I’ve been imagining for this little project:

  • More “analog” style: Braitenberg’s articles think of the little guys as wired up in an analog style, with larger or smaller signals going to the wheels. My guys get numbers added and subtracted but it feels more digital to me.
  • Connections mode: Braitenberg shows wiring diagrams and response curves. My program’s behaviors are written in Codea. Maybe there could be a wiring mode where different signal generators could be hooked together. That would be challenging at a few levels, I bet.
  • Autonomous Vehicles: Chet and I are talking today about our deliver:Agile talk, and one possible topic is that teams don’t know the design of their systems. That lead us to think about the extent to which these little bugs would make a sensible architecture for autonomous vehicles.

Looking at the code … this always happens … I see some room for improvement, so I quickly do it.

function BVehicle1:behaveSeekFood()
    return vec2(self:seekFood())
end

function BVehicle1:seekFood()
    local lw = 0
    local rw = 0
    local food = vec2(food.x, food.y)
    local distL = food:dist(self.eyeL)
    local distR = food:dist(self.eyeR)
    local adj
    if distR > distL then 
        rw = 1
    else
        lw = 1
    end
    return lw, rw
end

No reason to do this in two steps, so let’s have this:

function BVehicle1:behaveSeekFood()
    local lw = 0
    local rw = 0
    local food = vec2(food.x, food.y)
    local distL = food:dist(self.eyeL)
    local distR = food:dist(self.eyeR)
    local adj
    if distR > distL then 
        rw = 1
    else
        lw = 1
    end
    return vec2(lw, rw)
end

And then this:

function BVehicle1:behaveSeekFood()
    local food = vec2(food.x, food.y)
    local distL = food:dist(self.eyeL)
    local distR = food:dist(self.eyeR)
    if distR > distL then 
        return vec2(0,1)
    else
        return vec2(1,0)
    end
end

This is surely simpler. It’s worth nothing that this behavior will surely change in the future: there will probably be multiple occurrences of food, for example. That, we can foresee, will cause us to break out this function into one that somehow selects a food source to approach, and one, much like this one, that steers toward it. I could have decided to leave things as they are, because “we’re going to need” the breakout. I choose to leave things as simple as I can see to do, so long as they work.

This works: commit the code.

I found a function, computeWheelIncrement, that wasn’t used. Deleted it. Commit.

This is just nasty:

function BVehicle1:avoidWall()
    local minDist = 50
    local wheels = vec2(0,0)
    local distLeftEye, distRightEye
    local adjLeft, adjRight
    distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(1000, self.position.y))
    adjLeft = -minDist/distRightEye
    adjRight = -minDist/distLeftEye
    wheels = wheels + vec2(adjLeft, adjRight)
    distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(0, self.position.y))
    adjLeft = -minDist/distRightEye
    adjRight = -minDist/distLeftEye
    wheels = wheels + vec2(adjLeft, adjRight)
    distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(self.position.x, 0))
    adjLeft = -minDist/distRightEye
    adjRight = -minDist/distLeftEye
    wheels = wheels + vec2(adjLeft, adjRight)
    distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(self.position.x, 1000))
    adjLeft = -minDist/distRightEye
    adjRight = -minDist/distLeftEye
    wheels = wheels + vec2(adjLeft, adjRight)
    return wheels.x, wheels.y
end

Look at all that duplication! What we’re doing is getting the scaled power of each wall against each of our eyes, and adding it all up. We can extract some functions here. Let’s see. We go in with a vec2 consisting of the perpendicular distance from each of our eyes to a wall. We compute a vec2 adjustment to our wheels, and cumulate those adjustments. And it happens four times, which makes me at least imagine a loop wanting to show up. Let’s see about extracting a function to do the job. I’ll call it wallAdjustment for now. First pass:

function BVehicle1:wallAdjustment(point)
    local minDist = 50
    local distLeftEye, distRightEye = self:eyeDistancesFrom(point)
    return vec2(-minDist/distRightEye, -minDist/distLeftEye)
end

function BVehicle1:avoidWall()
    local minDist = 50
    local wheels = vec2(0,0)
    local distLeftEye, distRightEye
    local adjLeft, adjRight
    wheels = wheels + self:wallAdjustment(vec2(1000,self.position.y))
    distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(0, self.position.y))
    adjLeft = -minDist/distRightEye
    adjRight = -minDist/distLeftEye
    wheels = wheels + vec2(adjLeft, adjRight)
    distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(self.position.x, 0))
    adjLeft = -minDist/distRightEye
    adjRight = -minDist/distLeftEye
    wheels = wheels + vec2(adjLeft, adjRight)
    distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(self.position.x, 1000))
    adjLeft = -minDist/distRightEye
    adjRight = -minDist/distLeftEye
    wheels = wheels + vec2(adjLeft, adjRight)
    return wheels.x, wheels.y
end

Works. Commit. Then this:

function BVehicle1:wallAdjustment(point)
    local minDist = 50
    local distLeftEye, distRightEye = self:eyeDistancesFrom(point)
    return vec2(-minDist/distRightEye, -minDist/distLeftEye)
end

function BVehicle1:avoidWall()
    local wheels = vec2(0,0)
    wheels = wheels + self:wallAdjustment(vec2(1000,self.position.y))
    wheels = wheels + self:wallAdjustment(vec2(0,self.position.y))
    wheels = wheels + self:wallAdjustment(vec2(self.position.x,0))
    wheels = wheels + self:wallAdjustment(vec2(self.position.x,1000))
    return wheels.x, wheels.y
end

That’s nicer … how about we try to get rid of that local and all the storing into it, and maybe just return a vector? The behaveWall bit looks like this:

function BVehicle1:behaveAvoidWall()
    return vec2(self:avoidWall())
end

I’m starting to think I refactored these behaviors too far. I wonder why. Anyway, let’s make this work, removing the cast here and putting it into the other method:

function BVehicle1:behaveAvoidWall()
    return self:avoidWall()
end

function BVehicle1:avoidWall()
    return self:wallAdjustment(vec2(1000,self.position.y))
     + self:wallAdjustment(vec2(0,self.position.y))
     + self:wallAdjustment(vec2(self.position.x,0))
     + self:wallAdjustment(vec2(self.position.x,1000))
end

Commit. Then roll avoid up into behave …

function BVehicle1:behaveAvoidWall()
    return self:wallAdjustment(vec2(1000,self.position.y))
     + self:wallAdjustment(vec2(0,self.position.y))
     + self:wallAdjustment(vec2(self.position.x,0))
     + self:wallAdjustment(vec2(self.position.x,1000))
end

So this is nice and compact. Parts of me thinks “It’s less clear” but I think what I’m really saying is that with the code smushed down this far, it’s more visible that it was never clear. When it’s complicated, we allow lack of clarity because, well, it’s complicated. Now we have just a few lines so it’s more clear that it’s not clear enough. Right now, I don’t see how I’d improve it.

One possible confusion is that the behave methods all return a vec2 adjustment to wheels, and that’s not expressed anywhere. For now, I’ve no real idea how to make it better.

Commit. Here’s the whole BVehicle1 class:

BVehicle1 = class()

function BVehicle1:init(behaviors, x,y, angle)
    self.behaviors = behaviors
    self.position = vec2(x,y)
    self.angle = angle or 0
    self.width = 10
end

function BVehicle1:partial(f, ...)
    local a = {...}
    return function(s)
        return f(s,unpack(a))
    end
end

function BVehicle1:behaveDefaultMotion(m1,m2)
    return vec2(m1, m2)
end

function BVehicle1:behaveJitter(probability)
    if math.random() >= probability then
        return vec2(0,0)
    else
        return vec2(math.random()*10 - 5, math.random()*10 - 5)
    end
end

function BVehicle1:behaveSeekFood()
    local food = vec2(food.x, food.y)
    local distL = food:dist(self.eyeL)
    local distR = food:dist(self.eyeR)
    if distR > distL then 
        return vec2(0,1)
    else
        return vec2(1,0)
    end
end

function BVehicle1:behaveAvoidWall()
    return self:wallAdjustment(vec2(1000,self.position.y))
     + self:wallAdjustment(vec2(0,self.position.y))
     + self:wallAdjustment(vec2(self.position.x,0))
     + self:wallAdjustment(vec2(self.position.x,1000))
end

function BVehicle1:calcAngle(wheels)
    return math.atan(wheels.y-wheels.x, self.width)
end

function BVehicle1:determinePosition(wheels)
    self.angle = self.angle + math.deg(self:calcAngle(wheels))
    local step = vec2((wheels.x+wheels.y)/2,0)
    local move = step:rotate(math.rad(self.angle))
    self.position = self.position + move
end

function BVehicle1:wallAdjustment(point)
    local minDist = 50
    local distLeftEye, distRightEye = self:eyeDistancesFrom(point)
    return vec2(-minDist/distRightEye, -minDist/distLeftEye)
end

function BVehicle1:eyeDistancesFrom(aPoint)
    return self:eyeDistanceLimited(self.eyeL, aPoint, 100), self:eyeDistanceLimited(self.eyeR, aPoint, 100)
end

function BVehicle1:eyeDistanceLimited(eye, aPoint, limit)
    local d = eye:dist(aPoint)
    if d > limit then return 10000 end
    return d
end

function BVehicle1:keepOnPage()
    self.position.x = math.fmod(self.position.x + 1000, 1000)
    self.position.y = math.fmod(self.position.y + 1000, 1000)
end

function BVehicle1:locateEyes()
    local eyeR = vec2(20,0)
    local eyeL = vec2(20,10)
    self.eyeR = eyeR:rotate(math.rad(self.angle)) + self.position
    self.eyeL = eyeL:rotate(math.rad(self.angle)) + self.position
end

function BVehicle1:update()
    self:locateEyes()
    local wheels = vec2(0,0)
    for i,b in pairs(self.behaviors) do
        wheels = wheels + b(self)
    end
        
    self:determinePosition(wheels)
end

function BVehicle1:draw()
    pushStyle()
    pushMatrix()
    rectMode(CENTER)
    stroke(255,255,0)
    fill(255,255,0)
    self:keepOnPage()
    translate(self.position:unpack())
    rotate(self.angle)
    rect(0, 0, 20, 10)
    local eyeSize = 8
    local eyeR = vec2(10,-5)  changed
    local eyeL = vec2(10,5)   changed
    stroke(255,0,0)
    fill(255,0,0)
    ellipse(eyeR.x, eyeR.y, eyeSize)
    ellipse(eyeL.x, eyeL.y, eyeSize)
    popMatrix()
    popStyle()
end

All this, including writing the article to this point, took about an hour, so it’s not like I’m wasting my life here. But I do think it’s time to work on some new behavior. Of the ideas above, two are quite grand, wiring diagrams and the application of these guys to autonomous vehicles in the real world. One idea was to consider whether the workings of our wheels are sufficiently analog, and I think they are. We add and subtract some pretty abstract values to the wheels, pushing them forward or back. If we think of those values as voltages, that’s fine. So I’m going to let that ride. Now let’s watch the guys run, and see what we might like to try next:

running