Monday, Monday …

Here we are again. Last time, we “invented” elementary pluggable behavior. I think we finished converting all the behaviors to pluggable, but we’ll look to make sure. My theory for the week’s work is to train the little guys to avoid the walls, and to set up some of them to fear the others, and other little behaviors that I may think of, just to see them running around on the screen. As always, I’ll be winging it, because I like to find out what the code teaches me, and to show how, with small enough steps,we can proceed safely and always stay in the range of pretty decent code.

First a review of the vehicle:


BVehicle1 = class()

function BVehicle1:init(x,y, angle)
    self.position = vec2(x,y)
    self.angle = angle or 0
    self.width = 10
    self.behaviors = {}
    table.insert(self.behaviors, self:partial(BVehicle1.behaveDefaultMotion, 1, 1.1))
    table.insert(self.behaviors, BVehicle1.behaveJitter)
    table.insert(self.behaviors, BVehicle1.behaveSeekFood)
end

function BVehicle1:partial(f, arg1, arg2)
    return function(...)
        return f(self, arg1, arg2,...)
    end
end

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

function BVehicle1:behaveJitter()
    return vec2(math.random()*4 - 2, math.random()*4 -2)
end

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

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:seekFood()
    local lw = 0
    local rw = 0
    local food = vec2(food.x, food.y)
    local eyeR = vec2(20,0)
    local eyeL = vec2(20,10)
    eyeR = eyeR:rotate(math.rad(self.angle)) + self.position
    eyeL = eyeL:rotate(math.rad(self.angle)) + self.position
    local distL = food:dist(eyeL)
    local distR = food:dist(eyeR)
    local adj
    if distR > distL then 
        rw = 1
    else
        lw = 1
    end
    return lw, rw
end

function BVehicle1:update()
    local wheels = vec2(0,0)
    for i,b in pairs(self.behaviors) do
        wheels = wheels + b(self)
    end
        
    self:determinePosition(wheels)
    
     useless check for going off screen
    if self.position.x > 1000 then self.x = 100; self.y = 100 end
end

function BVehicle1:draw()
    pushStyle()
    pushMatrix()
    rectMode(CENTER)
    stroke(255,255,0)
    fill(255,255,0)
    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(d)
end

Oddly enough, my recollection that all the behaviors were done is accurate. So that’s good. Of course, avoiding walls should also be a behavior, and the current avoidance code is useless, so let’s do wall avoidance. Since our only little guy right now just walks more or less straight to the food, I’ll need a better test creature. Let’s just give him a slightly curving path and see what happens.

Ah! Easier said than done. Look at our setup:

function setup()
    print("Braitenberg Vehicle Experiment")
    vehicle = BVehicle1(400,400, 0)
    food = Food(300,900)
end

We only have one vehicle, so we’ll need a collection of them to update. Probably the food should go in there too, but we’ll worry about that later. In addition, all vehicles are created with the same setup, other than coordinates, so we need some way to select what they do. First things first, create and run two vehicles:

 Br1

function setup()
    print("Braitenberg Vehicle Experiment")
    objects = {}
    table.insert(objects, BVehicle1(400,400,0))
    table.insert(objects, BVehicle1(800,200, 45))
    food = Food(300,900)
end

 This function gets called once every frame
function draw()
    food:update()
    for _,v in pairs(objects) do
        v:update()
    end
    
     This sets a dark background color 
    background(40, 40, 50)
    

     Draw the playground
    strokeWidth(5)
    stroke(255,0,0)
    fill(0,0,0,0)
    rect(0,0, 1091, 1024)
    stroke(255,255,255)
    fill(50,50,50)
    rect(0,0,1000,1000)
    
     draw the world
    stroke(255,0,0)
    fill(255,0,0)
    ellipse(400,400, 10)
    food:draw()
    for _,v in pairs(objects) do
        v:draw()
    end
end

This was straightforward. I just created a table objects, dropped two vehicles into it, and changed draw to loop over all objects for update and draw. This gives me two hungry bugs, who look like the picture below. The little guy at lower right is the new one. He starts off at a 45 degree angle, but immediately turns for food.

Now to figure out how to handle variable behavior packages. This is starting to sound like we need some new kind of “behavior” object, but, for me, it’s too soon to say. Unquestionably, though, we need to move behavior creation up into setup, at least for now, and to create our vehicles each with their own behavior package.

My plan now is to pull the short behavior-table creating code up to setup, while providing another parameter for vehicles, passing that table in.

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

And a fairly big mistake! I should have committed the previous change. In moving the behavior creation code to setup, I somehow lost the copy buffer and now the code is gone. But I can commit, revert, then come back, I guess. Grr. Frequent commits, Ron! Don’t forget.

Ah, it was slightly easier because Working Copy has the old code, since I didn’t compile yet, so it doesn’t see my pending changes. I can carry on. My first idea was to create the vehicles including a behavior collection, in one go, following Beck’s Complete Creation Method pattern, which dictates that we never create an object that isn’t entirely ready to go. The code looks like this:

function setup()
    print("Braitenberg Vehicle Experiment")
    objects = {}
    b1 = {}
    table.insert(b1, BVehicle1:partial(BVehicle1.behaveDefaultMotion, 1, 1))
    table.insert(objects, BVehicle1(b1, 800,200, 45))
    b2 = {}
    table.insert(b2, BVehicle1:partial(BVehicle1.behaveDefaultMotion, 1, 1.1))
    table.insert(b2, BVehicle1.behaveJitter)
    table.insert(b2, BVehicle1.behaveSeekFood)
    table.insert(objects, BVehicle1(b2, 400,400,0))
    food = Food(300,900)
end

I create two behavior tables, because if I created just one and put different behaviors in it between creations, all the vehicles would wind up with the same behaviors, because they’d have identical tables. How do I know? Because I made that mistake, of course.

So this works, with the vehicle at 800,200 driving up at 45 degrees until he drives offscreen:

offscreen

You’ll notice also that I call BVehicle1:partial to plug in the parameterized behavior defaultMotion. That works because partialis implemented to pass in self, which is evaluated when the individual vehicle calls the behavior. If you find that hard to picture, you’re not alone. Anyway, it works. I’m not entirely happy with this setup, but I’m deciding to go ahead with the new behavior, and then see what needs cleaning up.

How to veer away from walls?

We veer toward food simply by assessing which eye sees food better and turning that way. I’ll try the same thing for walls, but there’s a concern in my mind. Since we’ll see all the walls if we look for them, we may have to give nearby walls more “power” than others. Or maybe I’ll just ignore walls further away than some amount. Yeah, that’ll do for a first cut.

If you’ve read the Braitenberg book, you’ve seen that he notionally adds different behaviors and then observes what would happen. His point is different from mine here, but I have in mind a similar experimental / evolutionary way of proceeding. I’d like to get in the mode of imagining new behaviors, coding them up, and seeing what happens. So for a while, let’s do that.

Our food-seeking behavior uses this code:

function BVehicle1:seekFood()
    local lw = 0
    local rw = 0
    local food = vec2(food.x, food.y)
    local eyeR = vec2(20,0)
    local eyeL = vec2(20,10)
    eyeR = eyeR:rotate(math.rad(self.angle)) + self.position
    eyeL = eyeL:rotate(math.rad(self.angle)) + self.position
    local distL = food:dist(eyeL)
    local distR = food:dist(eyeR)
    local adj
    if distR > distL then 
        rw = 1
    else
        lw = 1
    end
    return lw, rw
end

Here, we compute our eye positions, and then each eye’s distance from the food, using that to decide which wheel to turn faster. I see that we’re going to duplicate the eye position code and that we’ll need to refactor that out again but for now, let’s drain the swamp. I’ll code a wall avoider much like seek food but doing all the walls. I’ll start with just one wall.

function BVehicle1:avoidWall()
    local lw = 0
    local rw = 0
    local eyeR = vec2(20,0)
    local eyeL = vec2(20,10)
    eyeR = eyeR:rotate(math.rad(self.angle)) + self.position
    eyeL = eyeL:rotate(math.rad(self.angle)) + self.position
    local dLright = 1000-eyeL.x
    local dRright = 1000-eyeR.x
    local adj
    if dLright > dRright then 
        rw = 1
    else
        lw = 1
    end
    return lw, rw
end

This is pretty funny. The little guy starts toward the right-side wall, then immediately turns left and steers directly away, heading for the left wall. And, of course he goes right over the edge. We’ll do something about that tomorrow.

Tuesday, bloody Tuesday

OK. The simplest thing to do is to duplicate our existing logic for all four walls. I anticipate that this will make our vehicle so confused that it will stay where it is but we’ll see.

function BVehicle1:avoidWall()
    local lw = 0
    local rw = 0
    local eyeR = vec2(20,0)
    local eyeL = vec2(20,10)
    eyeR = eyeR:rotate(math.rad(self.angle)) + self.position
    eyeL = eyeL:rotate(math.rad(self.angle)) + self.position
    local dLright = 1000-eyeL.x
    local dRright = 1000-eyeR.x
    local adj
    if dRright > dLright then  right eye closer to right wall
        rw = 1                 turn left (by incrementing right wheel)
    else
        lw = 1
    end
    dLleft = eyeL.x
    dRleft = eyeR.x
    if dLleft < dRleft then  left eye closer to left wall
        lw = lw + 1          then turn right by incrementing left wheel
    else
        rw = rw + 1
    end
    return lw, rw
end

OK well that’s confusing to write and read, isn’t it? Worse yet, the two cancel out. If your right eye is closer to the right wall, the first bit makes you turn left by incrementing the right wheel more. But your left eye is therefore closer to the left wall, and you turn right by incrementing the left wheel. So the net result is you go straight (but a bit faster).

This won’t do at all. The “d” for distance “R” for right eye “right” for right wall is hard to understand and hard to get right (pardon me). The incrementing of the right wheel to turn left is hard to remember. I do want the turning to be in the wheel speeds, to better match Braitenberg’s model, but we’ve got to fix the notation. First let’s see if we can make a little progress by taking the distance from the wall into account. I’ll try setting the increment to some number divided by the distance. What should some number be? Let’s say that if he’s within 50 units of the wall the force should be full on. So I’ll use 50 / distance to start.

function BVehicle1:avoidWall()
    local minDist = 50
    local lw = 0
    local rw = 0
    local eyeR = vec2(20,0)
    local eyeL = vec2(20,10)
    eyeR = eyeR:rotate(math.rad(self.angle)) + self.position
    eyeL = eyeL:rotate(math.rad(self.angle)) + self.position
    local dLright = 1000-eyeL.x
    local dRright = 1000-eyeR.x
    local adj
    if dRright < dLright then  right eye closer to right wall
        rw = minDist/dRright                 turn left (by incrementing right wheel)
    else
        lw = minDist/dRright
    end
    dLleft = eyeL.x
    dRleft = eyeR.x
    if dLleft < dRleft then  left eye closer to left wall
        lw = lw + minDist/dRleft          then turn right by incrementing left wheel
    else
        rw = rw + minDist/dRleft
    end
    return lw, rw
end

This is nearly good. Note that I had the sense of the first comparison wrong and had to fix it (again). Now the little guy heads away from the right wall, toward the left, then arcs back toward right, and so on. Meanwhile he’s drifting up and goes off the top. I could, with some effort and trial and error, duplicate this logic for up and down and keep him in bounds. And I’m interested to see what he’ll do when I do that. But this code is too hard to write and modify. So I’m going to commit this and then refactor.

    local dLright = 1000-eyeL.x
    local dRright = 1000-eyeR.x
    local adj
    if dRright < dLright then  right eye closer to right wall

The above code answers whether the right eye is closer to the right wall. We’re interested in questions of this kind often: is the right eye closer to some point (like the food or the wall) than the left. Maybe I can abstract that out and use it here. The point on the wall closest to us will always be, for example, <1000,y> if our position is <x,y>, so we can use a generic isRightEyeCloser function. Probably. I’ll write it and try it out.

… time passes …

Bad news. Very bad news. I’ve just messed this up more and more. It’s clear I’m in a bad place and thrashing. What should we do when we’re this confused? We should roll back to the last place we were OK, and start again. Here goes.

Reflection

What I did was write the function rightEyeIsCloserTo(aPoint) and use it. Here’s a place where some tests might help me, because I’m not certain whether the function didn’t work, or whether I didn’t use it correctly. Along the way, I also had vehicles crawling off screen, and I really rather wanted to observe what they did, and really didn’t want them to leave the screen. So I’d like to wrap them around to keep them on the screen. I think I’ll leave the wall avoidance as it is, and solve the running off problem first:

Yay, a small success:

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(d)
end


function BVehicle1:keepOnPage()
    if self.position.x < 0 then self.position.x = self.position.x + 1000 end
    if self.position.x > 1000 then self.position.x = self.position.x - 1000 end
    if self.position.y < 0 then self.position.y = self.position.y + 1000 end
    if self.position.y > HEIGHT then self.position.y = self.position.y - 1000 end
end

There’s probably a better way to do that but this works and keeps the little buggers on the page. I wonder if x+1000 mod 1000 would give the same effect. Let’s try it, but first we commit. Then … yes this works:

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

I guess that’s better.At least it isn’t a if statement, which always bugs some of my friends. Commit again.

Enough

That’s an hour’s work. We have a sketch of keeping away from the side walls, and we keep the little guys on the page. We are not impressed with our performance, but we also know when it’s time to take a break. See you next time!

Wednesday

Well, it’s a new day. Let’s see what we can see. We reverted back to an ugly version of keeping away from the wall:

function BVehicle1:avoidWall()
    local minDist = 50
    local lw = 0
    local rw = 0
    local eyeR = vec2(20,0)
    local eyeL = vec2(20,10)
    eyeR = eyeR:rotate(math.rad(self.angle)) + self.position
    eyeL = eyeL:rotate(math.rad(self.angle)) + self.position
    local dLright = 1000-eyeL.x
    local dRright = 1000-eyeR.x
    local adj
    if dRright < dLright then  right eye closer to right wall
        rw = minDist/dRright                 turn left (by incrementing right wheel)
    else
        lw = minDist/dRright
    end
    dLleft = eyeL.x
    dRleft = eyeR.x
    if dLleft < dRleft then  left eye closer to left wall
        lw = lw + minDist/dRleft          then turn right by incrementing left wheel
    else
        rw = rw + minDist/dRleft
    end
    return lw, rw
end

This does sort of work: our little bug wanders away from the left or right walls, starting when it gets within about a quarter of the screen width away from the wall. Meanwhile, it crawls up the screen slowly, because we’re not checking top and bottom yet. So the power of the wall is too high: we’d like to allow him to get closer, but that’s a detail. A more significant fact is that the code is just ugly.

I tried yesterday to write a useful function answering whether a point was closer to our right eye or our left, but it didn’t do the job. Part of the problem, surely, was that the naming of all the little values was confusing. Today, with what may be a more clear head, let’s try to do better.

First, we’re clearly going to want to know the distance from the bug, or the eyes, to the point in question. Various tropisms will lead us toward or away, and they will want to vary their effect depending on how far away the item is. Now I’m not much for “we’re gonna need this so let’s do it now”, but let’s keep in mind that a distance abstraction seems valuable.

Additionally, there’s all this repeated calculation about our eyes. Let’s see if we can reduce that duplication, by calculating those positions and storing them as member variables. That could go early in update. So what if we had a function something like distancesFrom(aPoint), returning a vector with the two distances from the eyes? Then we could do whatever we need to with that.

I’ll push in that direction, and I’m going to go in the smallest steps I can think of. First the eyes:

function BVehicle1:update()
    self:locateEyes() <- this is new
    local wheels = vec2(0,0)
    for i,b in pairs(self.behaviors) do
        wheels = wheels + b(self)
    end
        
    self:determinePosition(wheels)
    
     useless check for going off screen
    if self.position.x > 1000 then self.x = 100; self.y = 100 end
end

I intend this method to define self.eyeL and eyeR’s location. I also note that update needs some refactoring using Composed Method. We’ll address that later unless I forget.

Anyway, here’s locateEyes:

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

That was a pure Extract Method from the avoidWall function (and the food function too). Now I should be able to change one of those to use this method. I’ll do the food one, because it’s more stable. It starts like this:

function BVehicle1:seekFood()
    local lw = 0
    local rw = 0
    local food = vec2(food.x, food.y)
    local eyeR = vec2(20,0)
    local eyeL = vec2(20,10)
    eyeR = eyeR:rotate(math.rad(self.angle)) + self.position
    eyeL = eyeL:rotate(math.rad(self.angle)) + self.position
    local distL = food:dist(eyeL)
    local distR = food:dist(eyeR)
    local adj
    if distR > distL then 
        rw = 1
    else
        lw = 1
    end
    return lw, rw
end

And ends up like this:

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

That “should” work. Let’s find out. And it does! I’m going to commit right now, even though I plan to use this method elsewhere. Let’s ratchet to a better place while we can. Done. Now to improve the other function:

function BVehicle1:avoidWall()
    local minDist = 50
    local lw = 0
    local rw = 0
    local dLright = 1000-self.eyeL.x
    local dRright = 1000-self.eyeR.x
    local adj
    if dRright < dLright then  right eye closer to right wall
        rw = minDist/dRright                 turn left (by incrementing right wheel)
    else
        lw = minDist/dRright
    end
    dLleft = self.eyeL.x
    dRleft = self.eyeR.x
    if dLleft < dRleft then  left eye closer to left wall
        lw = lw + minDist/dRleft          then turn right by incrementing left wheel
    else
        rw = rw + minDist/dRleft
    end
    return lw, rw
end

That’s a bit better. Commit again.

Now we see a bit more clearly that what avoidWall is doing is adding a bit to the right or left wheel, depending on that wheel being closer to the wall. Spinning the wheel that’s closer to the wall faster turns us away from the wall. And we’re using the distances in avoidWall so it makes sense to encapsulate getting them, and to return them as a vector.

I also notice that this function is going to be made up of “avoid right wall”, “avoid left wall”, “avoid top wall”, “avoid bottom wall”. I’m torn, because I want to leap to that implementation. I’m sure it will be easy. I’m also reminded that I’m not very bright, and it won’t be any harder to do it in tiny steps. Since I’m thinking in terms of a vector of distances to a point, let’s first convert this function to think in terms of vectors. We’ll really just create them and then probably pick them apart again, but that will set us up for the creation to be done elsewhere.

Or … we could have our function return, not a vector, but a pair of items. I think Lua can just do that, and so let’s try it that way. It feels like a bit less packing and unpacking.

Here’s what I did:

function BVehicle1:avoidWall()
    local minDist = 50
    local lw = 0
    local rw = 0
    local dLright, dRright = self:eyeDistancesFrom(vec2(1000, self.position.y))
    local adj
    if dRright < dLright then  right eye closer to right wall
        rw = minDist/dRright                 turn left (by incrementing right wheel)
    else
        lw = minDist/dRright
    end
    dLleft = self.eyeL.x
    dRleft = self.eyeR.x
    if dLleft < dRleft then  left eye closer to left wall
        lw = lw + minDist/dRleft          then turn right by incrementing left wheel
    else
        rw = rw + minDist/dRleft
    end
    return lw, rw
end

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

Now this is a bit different from what we had. The old code calculates the distance from each eye to the wall, using that eye’s own y coordinate and the wall’s x. This code is measuring the distance between the eye and the vehicle’s position opposite the wall. So our little guy actually winds up behaving a bit differently, turning downward instead of upward.

We could “fix” this in a number of ways, such as by having a distance-to-line function, or a more specialized function for the walls. I’m going to let it be, because I think it won’t matter. Let’s go ahead and use our new function and clean up the wall avoidance a bit more. But first, a commit. Now then:

function BVehicle1:avoidWall()
    local minDist = 50
    local lw = 0
    local rw = 0
    local dLright, dRright = self:eyeDistancesFrom(vec2(1000, self.position.y))
    local adj
    if dRright < dLright then  right eye closer to right wall
        rw = minDist/dRright                 turn left (by incrementing right wheel)
    else
        lw = minDist/dRright
    end
    local dLleft, dRleft = self:eyeDistancesFrom(vec2(0, self.position.y))
    if dLleft < dRleft then  left eye closer to left wall
        lw = lw + minDist/dRleft          then turn right by incrementing left wheel
    else
        rw = rw + minDist/dRleft
    end
    return lw, rw
end

That still seems to be working. Let’s normalize the usage and reuse the variables. Can you see how now we might like a vector notation better? I’m getting that feeling just a bit. We’ll see. Each of these steps takes less time to do than it does to write it up, so we’re doing fine.

Next step is this:

function BVehicle1:avoidWall()
    local minDist = 50
    local lw = 0
    local rw = 0
    local distLeftEye, distRightEye
    distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(1000, self.position.y))
    local adj
    if distRightEye < distLeftEye then  right eye closer to right wall
        rw = minDist/distRightEye                 turn left (by incrementing right wheel)
    else
        lw = minDist/distRightEye
    end
    distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(0, self.position.y))
    if distLeftEye < distRightEye then  left eye closer to left wall
        lw = lw + minDist/distRightEye          then turn right by incrementing left wheel
    else
        rw = rw + minDist/distRightEye
    end
    return lw, rw
end

Let’s commit. Now, what do we see here? There’s duplication (ish) in the if statements, if we’d reverse the sense of the second one. (That wouldn’t be exact, because we’ve been ignoring the case of the two values being equal right alone. As coded, we always turn. If we’re headed directly toward the wall, we turn a bit … um … right in both cases. Perhaps we’d like to have the case of equal do nothing. That would make more sense.

Anyway we could somehow extract out that if block. But first an alert.

We are not really refactoring here. Yes, we are improving the design of our code, at least I hope we are. But we are not quite preserving its function. We’re preserving our intention, arguably, but our intention is a bit loose here, isn’t it? Now this is a risky thing to do, changing function and design at the same time. We’ll probably do better to wear just the refactoring hat or the implementation hat, one at a time. We are in danger of looking funny when we wear both hats.

Now of course we are smart enough to do this and several other things at the same time. Yeah, well, that way leads to debugging, and debugging wastes time. So I’d better be cautious here and if I had a pair, they’d probably be arguing with me right now. So hold my beer and watch this: I might get in trouble. Still, so far so good.

First, I’m going to reverse that second if, to make it just like the first. That will change behavior a bit, maybe noticeably. We’ll see.

function BVehicle1:avoidWall()
    local minDist = 50
    local lw = 0
    local rw = 0
    local distLeftEye, distRightEye
    distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(1000, self.position.y))
    local adj
    if distRightEye < distLeftEye then  right eye closer to right wall
        rw = minDist/distRightEye                 turn left (by incrementing right wheel)
    else
        lw = minDist/distRightEye
    end
    distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(0, self.position.y))
    if distRightEye < distLeftEye then  left eye closer to left wall
        rw = rw + minDist/distRightEye
    else
        lw = lw + minDist/distRightEye          then turn right by incrementing left wheel
    end
    return lw, rw
end

No behavior change that I can notice. Commit. Now let’s think about how to extract that if. Each branch adds the same value to the eye that’s less. What if we had a function that returned a pair of values, one of which is zero, and the other a provided value, given two comparands? I’ll try that and see if I like it. Also I’m going to try something tricky, namely passing both results of eyeDistancesFrom to our new function. Definitely hold my beer for this one, I’ve never done this before that I can recall.

Well. I couldn’t quite do as intended but I did do this:

function computeWheelIncrement(value, left, right)
    if right < left then
        return 0, value
    else
        return value, 0
    end
end

function BVehicle1:avoidWall()
    local minDist = 50
    local lw = 0
    local rw = 0
    local distLeftEye, distRightEye
    distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(1000, self.position.y))
    lw, rw = computeWheelIncrement(minDist/distRightEye, distLeftEye, distRightEye)
    distLeftEye, distRightEye = self:eyeDistancesFrom(vec2(0, self.position.y))
    if distRightEye < distLeftEye then  left eye closer to left wall
        rw = rw + minDist/distRightEye
    else
        lw = lw + minDist/distRightEye          then turn right by incrementing left wheel
    end
    return lw, rw
end

I had to do it in two steps, because I don’t know the value to pass in until I get the values back from the eye distance routine. In this first case, I just assign lw and rw In the second usage, though, I’m going to have to add the result to them. First, though, a commit.

Doing the incrementing will be easier if we treat the two wheels as a vector here. I’ll do that, then use the increment function a second time:

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

Now this is looking rather nice. Commit.

A quick review of the whole situation shows me that the people who use avoidWall, and seekFood, and so on, are actually working with vectors anyway. Our “behave” wrappers all convert their output to vectors, and our main behavior-calling loop assumes vectors, summing them into a local wheels vector. If we do a bit more work, we can pull out some more of the pair-wise thinking and consolidate on vector thinking. That will reduce the code, and simplify it, I expect. We’ll do that next time.

Summing up Wednesday

In a bit more than an hour, I’ve written this material and improved the program substantially. I feel that I’ve redeemed myself after the debacle yesterday. This is often the way things go. On the C3 project of old, we used to say that what took us hours to do wrongly yesterday will take only a few minutes to do today. That seems to be borne out today.

I’m calling the day over and heading for lunch. See you next time!