Braitenberg Vehicles - top
I experimented a bit today with Working Copy and Codea to see if a different way of referring to the project would give smoother reverts and checkouts. As it stands, a revert or checkout in Working Copy seems only to be reflected in Codea when you close and reopen the project. The new idea, provided by Simeon of Codea, was to put the project into Working Copy instead of Codea, and open it from there. That turns out not to solve the update problem, and to be harder to use. So I’ve reverted back. :)
This morning, I think I’ll fiddle a bit with jittering. Currently, all the vehicles jitter every ten updates, so they all jitter together. It seems reasonable that some of them would be more nervous than others, so my plan is to make them jitter with some probability, and to provide that probability as a parameter to the jitter call. Should be a straightforward use of the new partial
capability.
Presently we have:
function BVehicle1:behaveJitter()
self.jitterCount = self.jitterCount + 1
if math.fmod(self.jitterCount, 25) == 0 then
return vec2(math.random()*10 - 5, math.random()*10 - 5)
else
return vec2(0,0)
end
end
Let’s make behaveJitter
expect a float parameter, and pull a random number to decide whether to jitter:
function BVehicle1:behaveJitter(probability)
if math.random() >= probability then return vec2(0,0) end
return vec2(math.random()*10 - 5, math.random()*10 - 5)
end
With initialization:
bAvoidWalls = {}
table.insert(bAvoidWalls, BVehicle1:partial(BVehicle1.behaveDefaultMotion, 1, 1))
table.insert(bAvoidWalls, BVehicle1:partial(BVehicle1.behaveJitter, 0.10))
table.insert(bAvoidWalls, BVehicle1:partial(BVehicle1.behaveAvoidWall))
bSeekFood = {}
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveDefaultMotion, 1, 1.1))
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveJitter, 0.50))
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveSeekFood))
Note that the wall avoiders use probability 0.10, and the food seekers, 0.5. So the one food seeker should be more nervous, while the wall avoiders should jitter at random times rather than in sync. And that’s what happens:
Let’s commit that, and then improve the code.
When I wrote it, I thought a Guard Clause pattern would be good for behaveJitter
, but now I don’t like it:
function BVehicle1:behaveJitter(probability)
if math.random() >= probability then return vec2(0,0) end
return vec2(math.random()*10 - 5, math.random()*10 - 5)
end
I think a simple if-then-else will read better given that we just have two cases. So …
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
I like that better, and hope that you do as well. I also changed the food-seeker to be a bit less jittery, reducing his change of jittering to 0.25.
I’m tempted to parameterize the size of the jitter as well, but there’s not much real point to that. This change increases my belief in my new partial
function, as well as providing just a little something to do this morning.
Some general remarks
I rather hope that there are some actual points to these articles, and the main one, to me at least, is that by doing tiny changes to improve our program, all the time rather than in big batches, we keep it clean, and we move it smoothly from not needing or having much of a design, to having a better and better design.
I moved fairly quickly to having a Vehicle object, which was pretty obvious. Similarly, putting all the live objects into an objects
table is a pretty clear thing to do in Codea, where you have a centralized call to draw
, which always needs to update all the objects in the universe, and then draw them. So after you get to about two objects, the update loop followed by draw loop just makes sense.
Creation of the Food object then just sort of makes sense, even though, right now, the Food isn’t very smart. I imagine that we’ll let food be consumed by nearby bugs, and then go quiescent or move somewhere else, to make things more interesting. And our Food class, created just to put it inside objects
, will probably serve as a basis for that. Future Food will probably have some creation parameters to control whatever behavior seems fun.
But the main lesson, so far, is to observe what I do, which is very small steps (except when I screw up), moving to the next small step in functionality, followed as soon as I notice concerns, by cleanup. Usually that cleanup is close to as simple as what we just did with behaveJitter
, little more than formatting a method or refactoring a few calls. Sometimes it’s more elaborate, like the partial
function, which actually required a bit of research and discovery. Even so, it was very localized, and that’s not an accident.
Getting behavior localized is of course a large-scale design principle, represented in notions of Coupling and Cohesion, or Single Responsibility Principle. It’s created, however, almost automatically, when we pay attention to clarity and duplication. When something is done multiple times, it’s asking to be pulled out, extracted, into a function or method. When a method starts getting so complex that we want to put comments into it saying “now we’re doing this, now we’re doing that”, it’s asking to have the individual meaningful components pulled out, and for the Composed Method pattern to be applied.
What surprised me two decades ago, and still surprises me today, is how much good large-scale design seems just to “emerge” from this very localized work.
Warning, though. This doesn’t mean “just happens automatically”. On the contrary, when I see something repeated, I ask myself “what is this?”. I think about what it is, and pull in all the design thinking I have in my head. And, believe me, after over a half-century of programming, I have literally two or three bits of useful design thinking, maybe more. So I think a bit about all the objects and functions and how they might go together, and then I push the code, just a bit, in that direction.
I try never to create a structure, a function, a class, or anything before its time. When is its time? When I actually need it right now, and, when I’m on my game, never just because “I’m going to need this”. Sometimes I’m more on my game than other times, but generally I stick really close to local needs. I do this to discover for myself how little pre-creation of architecture I can get away with, and I write about it in the hope of enticing you to hold off a bit on building things for the future.
My thoughtful experienced view is that clean well-structured code is quite ready enough for the future. But to keep it that way does require close attention to things getting even a bit crufty, and that requires a lot of practice. And, to be fair, although it doesn’t say anywhere in our agreement that I’ll be fair, it is at least possible that I do as well as I do, not just because careful local action leads to maintainable code, but because my design intuition is strong enough to keep me from making too many hideous irrecoverable errors. I don’t think that’s the case: I think you can do this too. But do pay attention. I could be wrong.
But I’m not. :)
See you next time!