Robots 1, I started at zero.
Day 2. I did watch a couple of GeePaw’s videos. Good news. Thinking ensues. Concerned about Knowledge.
I watched a couple of GeePaw Hill’s Robot Worlds videos last night, to see what he had to say about the socket connections between robots and world. There does exist a socket library for Lua, and I though I might try to learn a bit about it. What, if anything, I do about that remains to be seen.
The good news is that Hill determined (no surprise here) that one can replace a client-server connection pair with a single direct connection. This discovery tells him that he can, if he chooses, do most of his work using an easy and fast direct connection. It tells me that what I do here can be as close to his spec as I choose to make it, and that I can defer using a socket connection as long as I choose, even forever. We’ll have more to say about the connection later on in the series.
Now the original We Think Code Robot Worlds exercise is intended to teach students what they need to know about real world programming, so it requires them to create a client-server setup, and to use JSON, and various other practical learning choices. We here chez Ron are not so constrained, and I think we’re better for it, in that we can think less about plumbing and more about the real program. It is too easy to let the plumbing and the program get tangled up, and that is never (as far as I know) a good thing.
Let’s get started.
Knowledge: Thinking
If I recall what I did yesterday—always an iffy matter—I developed a Knowledge class that currently contains a simple table of GridCell instances, each of those having an x, a y, and a content. (I’m writing this before I look at the code, in an attempt to get my mind clear on how the thing works.)
The knowledge instance is created with a starting x,y of its own, at 0,0. It adjusts that position when told to. The idea here is that the robot does not know its absolute position, so it assumes, for want of a better idea, that it is as 0,0. So its knowledge assumes that as well. When the robot scans for information, that information comes back in relative coordinates. Something just to its left will have an x value of -1. Something just above will have a y value of +1. And so on. When the robot does a scan, it just dumps the results into its knowledge.
But the job of knowledge is to build up a map of everything we’ve ever scanned. (I’m assuming that the scan will be limited in range, so that remembering where things are, outside of range, will be useful. My game, my assumptions.)
Therefore, knowledge wants to store everything in coordinates that are not relative to the current position of the robot, but relative to its original position.
So let’s think about this in detail, still before we look at what the code actually does. I feel that the code doesn’t express very well what’s going on, and is therefore confusing.
Suppose when we start out (at 0,0), we do a scan, and we find an A to our immediate left. Its coordinate will be (-1,0). And suppose there is a B right above us. It will be at (0,1).
Now suppose that we move right and up. Our new origin-relative coordinates are (1,1). The B is now directly beside us, at (-1,0), and the A will be at (-2,1), relative to where we are now.
A: (-2,-1) = (-1,0) - (1,1)
B: (-1, 0) = ( 0,1) - (1,1)
Those are the values we should return if the robot asks its knowledge what’s around it. So when we move in a direction like (1,1), we should add that to our assumed current position, and when we are asked what’s at a given relative position, we should subtract our current position from the input position and look for that resulting value. Further, when we do an additional scan while we are at some position (x,y), we should add our current position to the incoming coordinates, so that they will be relative to 0,0 rather than to wherever we are now.
I drew a picture to help me think about this.
Now, there are other ways the knowledge could do things, but they will all require reciprocal adding and subtracting to adjust incoming and outgoing values.
Let’s see what it does now and whether we like it.
Knowledge: Implementation
Here’s all the code for our reference. I’ll break out bits below, so you can just scan this now, or skip it entirely if you are that kind of person.
-- TestStarter
-- RJ 20220608
function test_Knowledge()
_:describe("Knowledge", function()
CodeaUnit_Detailed = false
_:before(function()
end)
_:after(function()
end)
_:test("Knowledge retained", function()
local knowledge = Knowledge()
knowledge:addFact(GridCell(1,1,"A"))
knowledge:addFact(GridCell(1,3,"B"))
knowledge:addFact(GridCell(5,1,"C"))
knowledge:addFact(GridCell(4,4,"D"))
_:expect(knowledge:factCount()).is(4)
_:expect(knowledge:factAt(1,1)).is("A")
_:expect(knowledge:factAt(1,3)).is("B")
_:expect(knowledge:factAt(5,1)).is("C")
_:expect(knowledge:factAt(4,4)).is("D")
end)
_:test("Knowledge adjusted", function()
local knowledge = Knowledge()
knowledge:addFact(GridCell(1,1,"A"))
knowledge:addFact(GridCell(1,3,"B"))
knowledge:addFact(GridCell(5,1,"C"))
knowledge:addFact(GridCell(4,4,"D"))
_:expect(knowledge:factCount()).is(4)
knowledge:adjust(2,2)
_:expect(knowledge:factAt(-1,-1)).is("A")
_:expect(knowledge:factAt(-1,1)).is("B")
_:expect(knowledge:factAt(3,-1)).is("C")
_:expect(knowledge:factAt(2,2)).is("D")
end)
_:test("Knowledge adjusted twice", function()
local knowledge = Knowledge()
knowledge:addFact(GridCell(1,1,"A"))
knowledge:adjust(2,2)
_:expect(knowledge:factAt(-1,-1)).is("A")
knowledge:addFact(GridCell(1,1,"B"))
knowledge:adjust(1,1)
_:expect(knowledge:factAt(0,0)).is("B")
_:expect(knowledge:factAt(-2,-2)).is("A")
end)
_:ignore("Knowledge can have two things at same place", function()
local knowledge = Knowledge()
knowledge:addFact(GridCell(1,1,"A"))
knowledge:addFact(GridCell(1,1,"B"))
_:expect(knowledge:factAt(1,1)).is("A")
_:expect(knowledge:factAt(1,1)).is("B")
end)
end)
end
Knowledge = class()
function Knowledge:init()
self.x = 0
self.y = 0
self.facts = {}
end
function Knowledge:adjust(x,y)
self.x = self.x + x
self.y = self.y + y
end
function Knowledge:addFact(aFact)
table.insert(self.facts, aFact:adjust(self.x, self.y))
end
function Knowledge:factAt(x,y)
for i,fact in ipairs(self.facts) do
if fact.x - self.x == x and fact.y - self.y ==y then return fact.content end
end
return nil
end
function Knowledge:factCount()
return #self.facts
end
GridCell = class()
function GridCell:init(x,y,content)
self.x = x
self.y = y
self.content = content
end
function GridCell:adjust(x,y)
return GridCell(self.x+x, self.y+y, self.content)
end
I recall now that we have an ignored test, which is recording the issue about more than one thing at the same location. No problem, it’s ignored and separate, I believe, from coordinate calculations.
I’m going to skip right to the code. No, I really shouldn’t do that. What I should do is record my new understanding, which may or may not be the same as yesterday’s understanding, in a new little test.
_:test("Thursday understanding", function()
local knowledge = Knowledge()
knowledge:addFact(GridCell(-1,0,"A"))
knowledge:addFact(GridCell( 0,1,"B"))
knowledge:adjust(1,1)
_:expect(knowledge:factAt(-1, 0)).is("B")
_:expect(knowledge:factAt(-2,-1)).is("A")
end)
Test. Runs, after I fix a typo. So my understanding is good. Now let’s review how we coded it:
function Knowledge:init()
self.x = 0
self.y = 0
self.facts = {}
end
function Knowledge:adjust(x,y)
self.x = self.x + x
self.y = self.y + y
end
function Knowledge:addFact(aFact)
table.insert(self.facts, aFact:adjust(self.x, self.y))
end
function Knowledge:factAt(x,y)
for i,fact in ipairs(self.facts) do
if fact.x - self.x == x and fact.y - self.y ==y then return fact.content end
end
return nil
end
And we adjust facts here:
function GridCell:adjust(x,y)
return GridCell(self.x+x, self.y+y, self.content)
end
OK, that does what we said. To get the “true” coordinate after we’ve moved, we have to add our move to the relative coordinate.
I don’t like the word adjust
, especially not in Knowledge. We’re move the robot with move, and it adjusts its knowledge with that same move, and the word “adjust” is losing some meaning.
I just had a scary thought. What if when your robot is hit, it can be displaced to a new location, where it will not be told how far it moved. That would mean that our knowledge might still be true … but its current x and y would be wrong.
Of course we could “just” flush all our hard-earned knowledge and start over, but there’s also the possibility of scanning and then matching what we get to our knowledge, and determining from a pattern match where we are. Interesting. But I digress.
Let’s change adjust to moveBy
, which I prefer to plain move
, because it connotes that the input parameters are not a position but a step. There’s also starting to be a thought in my mind that we need objects for location and step but not just now.
A bit of find/replace and I’ve changed them all, even the call from Robot, and the method in GridCell. We can commit: rename adjust to moveBy.
I don’t like the name GridCell. In the code we’re calling them facts. Let’s rename the class to Fact. I trust Codea’s global replace with this easy task and it does not let me down. Tests good except for the ignored one. Commit: rename GridCell to Fact.
Hold on. I need sustenance. Day started oddly. Back in a bit.
What’s Next
I am all nice clean, have a banana going, Granola bar and Chai queued up, ready to go.
I’m thinking that the next thing will be to build out the world a bit, so that we can begin to work on a robot exploring the world. In back-channel conversations, I’ve learned a bit about how things go in the original spec, and I’ll try to be clear when I’m following that and when I’m doing something different. I might even say why.
Anyway, the world is a grid of coordinates, x and y, positive x pointing East and positive y pointing North. the objects in the world can be of type OBSTACLE, EDGE, MINE, ROBOT, or PIT. Robots take up one position. OBSTACLES are defined to be rectangular, but of course could be placed to make other shapes. (This seems to me to be irrelevant information, as you can only see one coordinate at a time.) Pits are also rectangular. Mines can be placed by Robots and do damage, which we’ll deal with later. Edges are the edge of the world.
It turns out that when you enter the world you are (apparently) able to find out your world coordinates. So that’s different from what I was supposing, but probably doesn’t matter yet.
Coordinates are, for your convenience, positive or negative, with the center of the world at 0,0. The world must be symmetric, which means that the width and height will always have to be constrained to odd values. Which is, well, odd.
Looking at these specs, I feel the way that Riddick must have when he was asked what he thought of the decor of the Necromongers’ ship and replied “I might have gone another way”.
But the spec is what it is, until it isn’t.
Let’s write some tests for the world.
The World
We have very little on the world so far:
-- RJ 20220608
function test_World()
_:describe("World", function()
CodeaUnit_Detailed = false
_:before(function()
end)
_:after(function()
end)
_:test("Create Default World", function()
local world = World()
end)
end)
end
World = class()
function World:test1()
return World()
end
function World:scan()
local knowledge = Knowledge()
knowledge:addFact(Fact(5,6,"fact"))
return knowledge
end
Do we even use the scan method? Yes, we do. When a robot does a scan, it sends scan
to the world and gets information back. Which reminds me:
According to the official spec, scan only looks forward, backward, left and right of the robot, not diagonally in any way. And information returned comes back in information relative to the robot’s position and rotation. So if the robot is at 0,0, an obstacle is at 2,0, and the robot is facing east, the scan will return something like {“OBSTACLE”, North, 2}. North being relative to the robot. Here again, I might have gone another way, perhaps with FORWARD, LEFT, RIGHT sort of thing. But it is what it is, until it isn’t.
So the Robot can turn left or right. We’ll need to deal with that, in due time. Right now I’m thinking about obstacles and reporting in the World.
Let’s think about world creation. The spec calls for the world’s definition to be read in from files or some such thing. We are not all about files here in Codea Lua, so we’ll provide some other kind of input to our objects, in the form that they like.
N.B. That’s actually important. Even if the spec calls for the world to be created from some bizarre form of JSON or something, we generally don’t write our objects to be running into JSON all the time. We write them to work in a fashion that is suitable to them, and we adapt from the information we get in, and adapt to the information we put out.
So, I’m going to provide information that is like what I understand to be coming in from outside, but in a form that I prefer. And, unless I’m awfully lucky this morning, I’ll be changing my internal forms frequently, to make them more to my liking.
It appears to me that there can only be one “thing” at a given location in the world. (But you can drive onto a mine, and drive into a pit. I’m not sure if you can drive over the edge or not.)
I think that what we’ll do, because we’re dealing with user input, is ensure that there can only be one thing in a given location. We have at least two options: we can just accept the last one, or we can emit an error of some kind when a duplicate comes along. I am inclined to the former, and, for now at least, it’s my game.
Let’s think about creating a world given a list of things to be put in the world. Something like this test:
_:test("Create small world with obstacles", function()
end)
You know what? That’s not small enough. Let’s deal with enforcing the dimensions. Width and height of the world must be odd. Center must be 0,0. If you give us an even size, what shall we do? We make the arbitrary decision to bump it up. Let’s write some tests about that first.
I’m going to do this in the smallest steps I can think of, to make a point. Let’s think what we’re about to make happen. By the time we’re done, if we create a world of size 3x5, its width and height will be 3 and 5. If we create one of size 5x7, its width and height will be 5 and 7. But if we create one of size 6x8, its width and height will be 7 and 9.
We’ll go one at a time, even though you could, and I probably could, do this all at once. But watch:
_:test("Create a 3x5 world", function()
local world = World(3,5)
_:expect(world:width()).is(3)
_:expect(world:height()).is(5)
end)
No, this isn’t even small enough. Try this one instead:
_:test("3x5 world width is 3", function()
local world = World(3,5)
_:expect(world:width()).is(3)
end)
OK this is silly small. When I test, I expect width not to be understood.
2: 3x5 world width is 3 -- TestWorld:20: attempt to call a nil value (method 'width')
OK, what is width? Well, it’s a method that returns 3, that’s pretty obvious:
function World:width()
return 3
end
I expect the test to pass. It does. Ship it!
This trick, returning a constant, is called “Fake It Till You Make It”. The great Kent Beck named it, to the best of my knowledge. Well this is obviously not done, so we write another test:
_:test("5x7 world width is 5", function()
local world = World(5,7)
_:expect(world:width()).is(5)
end)
This probably works, right? Well, no, it’s going to fail on the 3 not being 5:
3: 5x7 world width is 5 --
Actual: 3,
Expected: 5
We need a smarter object. The simplest change we can make is probably this:
function World:init(width)
self.width = width
end
function World:width()
return self.width
end
Note that I didn’t even look for or save the height. But now I expect this test to run correctly. But it doesn’t, because, as I so often do, I forget that I can’t name a member variable the same as a method. So … this:
function World:init(width)
self._width = width
end
function World:width()
return self._width
end
Now it should run … and it does. But why am I doing such silly things? Clearly I know that I need two member variables and two member functions and such and so. Surely I could do it all in one go. But sometimes I do not, and in fact, speaking just for me, I should do it a lot less than I do.
One of my flaws, as a very experienced and probably overly optimistic programmer, is that I often take too large a bite. Usually I get away clean. Sometimes I have a little trouble. Sometimes I fall into a very deep pit and have to dig out or revert. And we saw here that even with this tiny set of steps, I actually made a mistake, a very common one for me, where I name a member variable and member function the same. So a case could be made that I should have taken an even smaller step, but I can’t think of one.
So I commend this approach of ridiculously tiny tests to you, as an exercise, or even as a common practice. There’s very little cost to taking too small a step, and the cost of taking one that is too large is often quite high.
Try it: you might find it kind of fun. Or don’t try it: I’m not the boss of you.
As long as we’re here, let’s write the test for an even width:
_:test("6x8 world width is 7", function()
local world = World(6,8)
_:expect(world:width()).is(7)
end)
This is going to fail with 6 expecting 7:
4: 6x8 world width is 7 --
Actual: 6,
Expected: 7
Nice. So we need to bump the even value up. That’s excellent, because I wanted to talk about that. I’ve recently seen some code with, I forget what, divides, floors, ceils, and I don’t know what all, that converts even sizes to odd. I honestly couldn’t tell if the code was correct or not. I propose to write code that I can understand, if at all possible.
I’ll begin with another famous trick, Coding By Intention. We don’t write the algorithm, we first write what it does:
We replace this:
function World:init(width)
self._width = width
end
With this:
function World:init(width)
self._width = self:bumpUpEvenValues(width)
end
And then we write a blank method:
function World:bumpUpEvenValues(aNumber)
end
And we wonder how to do it. One way is like this:
function World:bumpUpEvenValues(aNumber)
if aNumber%2 == 0 then
return aNumber+1
else
return aNumber
end
end
I am rather sure that that works and, to my eyes, it’s pretty clear what it does. It also breaks two tests:
1: Create Default World -- TestWorld:15: attempt to perform arithmetic on a nil value (local 'aNumber')
2: Robot updates knowledge on move -- TestWorld:15: attempt to perform arithmetic on a nil value (local 'aNumber')
That, of course, is because we have a couple of folks just creating World with no parameters. Let’s decide that a world with no width and height will default to 1x1. For now that means that someone here needs to deal with a nil width:
function World:init(width)
self._width = self:bumpUpEvenValues(width)
end
function World:bumpUpEvenValues(aNumber)
if aNumber%2 == 0 then
return aNumber+1
else
return aNumber
end
end
The easiest way, is this Lua expression, which is quite common in Lua. In your favorite language, you might do it another way.
function World:init(width)
self._width = self:bumpUpEvenValues(width or 1)
end
I think something interesting may happen later on. We’ll see.
Now I expect my tests to run again. They do. Shall we commit? Sure, the code is better. Commit: World accepts width parameter, coerces it to odd.
Now, we could do exactly the same process for height, inch by inch, step by step, but I think that would truly be silly. Instead, this time, I’ll write a test that I think is about the right size:
_:test("6x8 world height is 9", function()
local world = World(6,8)
_:expect(world:height()).is(9)
end)
I’m going straight for the heart on this one. It will fail first on missing height:
5: 6x8 world height is 9 -- TestWorld:64: attempt to call a nil value (method 'height')
I implement that in one go:
function World:init(width, height)
self._width = self:bumpUpEvenValues(width or 1)
self._height = self:bumpUpEvenValues(height or 1)
end
function World:height()
return self._height
end
I expect the test to run. It does. Commit: World(w,h) defaults to 1x1, and is always of odd width and height.
We’re green. Let’s see if we would like to refactor any of this. Here’s World in all its glory:
World = class()
function World:test1()
return World()
end
function World:init(width, height)
self._width = self:bumpUpEvenValues(width or 1)
self._height = self:bumpUpEvenValues(height or 1)
end
function World:bumpUpEvenValues(aNumber)
if aNumber%2 == 0 then
return aNumber+1
else
return aNumber
end
end
function World:height()
return self._height
end
function World:scan()
local knowledge = Knowledge()
knowledge:addFact(Fact(5,6,"fact"))
return knowledge
end
function World:width()
return self._width
end
When we look at World:bumpUpEvenValues
, we see that it does not refer to self. That tells us that the method doesn’t really belong here in World. It is a function from number to number, and we intend it to be from integer to integer.
We should consider, since the world layout is coming from “outside” whether we need to protect ourselves against negative numbers and floats. We might also keep in mind that carrying around separate width and height or x and y is problematical, as at least the x and y are bound together pretty firmly as the world coordinates, so we may need a coordinate object.
But I think it’s too soon for that.
We might also look at this:
function World:bumpUpEvenValues(aNumber)
if aNumber%2 == 0 then
return aNumber+1
else
return aNumber
end
end
And ask whether there is a better way to implement the notion of bumping the number up if it is even. We might be the kind of person who likes to avoid if statements. We might think for a moment about
aNumber%2
That is zero if aNumber is even, and one if aNumber is odd. We kind of wish it could be one if aNumber is even and zero if it is odd. We might realize that this expression has those values:
1 - aNumber%2
So we could rewrite the method this way:
function World:bumpUpEvenValues(aNumber)
return aNumber + (1 - aNumber%2)
end
Test that. Tests run, no surprise. Do we prefer that formulation? It goes from five lines to one. Is it a bit less expressive? I would say that it is. But the name of the method tells us what it does and that should be enough of a clue for the reader.
YMMV, and you should do as you see fit. I’m going with this one, which can’t be objected to on the basic of efficiency or containing ifs.
Of course there is this one:
function World:bumpUpEvenValues(aNumber)
return aNumber + ((aNumber&1)~1)
end
I’ll leave it to the reader to understand why I’m not going to do that. It does pass the tests, though. We’ll stick with this:
function World:bumpUpEvenValues(aNumber)
return aNumber + (1 - aNumber%2)
end
By the way, you might wonder “why the redundant parentheses?” My answer is: Because they call the eye to the expression (1 - aNumber%2), helping us recognize “oh, that’s just 1 if even”.
OK. Enough refactoring and fun, commit: Replace 5 lines in bumpUpEvenValues with one line.
Reflection
Was all this worth it for one tiny method? Who’s to say. We’re here to look at, think about, and modify code, so we are happy to take the time. In a real app, I think it is definitely worth it to have the explanatory method name, and perhaps not worth it to replace the if version with the mod version.
Moving On …
Where are we? We’ve done one tiny story, which is that the World is always of odd width and height. We could stop for the day: it is nearly 1300 hours, but let’s do one more thing, dealing with inputs to world creation that are not positive integers. Let’s write a test:
_:test("fractions are lost", function()
local world = World(5.5, 6.5)
_:expect(world:width()).is(5)
_:expect(world:height()).is(7)
end)
Fun. Run the test. I am rather surprised by the fact that it actually runs. Why is that? I do not know. A quick search finds this:
Modulo in Lua is defined as a % b == a - math.floor(a/b)*b .
So that’s nice. Um, OK
5.5%2 is 5.5 - floor(5.5/2)2 and I’d guess floor(5.5/2)2 is floor(2.25)t is 22 is 4 so … this is too hard to do in my head. I want to understand this, so I’m going to do some prints. No, I’ll do some tiny tests instead. This is what it takes to pass:
_:test("Lua mod odd behavior", function()
local mod = 5.5%2
_:expect(mod).is(1.5)
end)
OK that’s fascinating and it would often work but I don’t trust it as far as I can understand it, which isn’t far. I think that if we didn’t have an even binary fraction here, might not work, we might have some roundoff. I’m going for code that visibly works, but I do wish I had something that would fail.
For now, this:
function World:bumpUpEvenValues(aNumber)
local int = aNumber//1
return int + (1 - int%2)
end
To me, that visibly works. You might be happy with some floors and such. I am not. Another test, for negatives. My rule is going to be that negatives are treated as zero.
_:test("negatives are zero", function()
local world = World(-51.43, -37.29)
_:expect(world:width()).is(1)
_:expect(world:height()).is(1)
end)
This better fail horribly.
8: negatives are zero --
Actual: -51.0,
Expected: 1
8: negatives are zero --
Actual: -37.0,
Expected: 1
Perfect. And now …
function World:bumpUpEvenValues(aNumber)
local nonNegativeInteger = math.max(0,aNumber)//1
return nonNegativeInteger + (1 - nonNegativeInteger%2)
end
And the tests are good. But the name of our method, not so good. What is a good name for this method? We originally gave it a name that helped us see what it does. Now we need a name that says what it means, and then we’re going to have another problem. Let’s go with a useful name:
function World:init(width, height)
self._width = self:validDimension(width or 1)
self._height = self:validDimension(height or 1)
end
function World:validDimension(aNumber)
local nonNegativeInteger = math.max(0,aNumber)//1
return nonNegativeInteger + (1 - nonNegativeInteger%2)
end
Now how it works isn’t quite clear, is it? Perhaps this is better?
function World:validDimension(aNumber)
local nonNegativeInteger = math.max(0,aNumber)//1
local bumpUpEvenValues = nonNegativeInteger + (1 - nonNegativeInteger%2)
return bumpUpEvenValues
end
The pattern is Explaining Variable Name, and I think it’s a bit better. Since I was faced with an expression for this that I couldn’t decode in my head, I want a definition for the thing that is as clear as I can make it. It’s rarely used, so efficiency is certainly no concern.
I’m going to ship it. Commit: Rename bumpUpEvenValues to validDimension, refactor for clarity.
Let’s reflect. Again.
Reflection (Again)
Is this tiny creation method and its supporting cast worth all this careful treatment? Well, I have to admit that on another day I might have been more careless, written larger and fewer tests, not worked so hard to get the thing as close to just right as possible.
And I also want to say right here in front of the world or robots and people, that the tiny code written today gives me both confidence that it does as I intend it to, and pride in that I’ve put enough of myself into it to make it be code that could be pasted on the classroom wall with my name on it and I’d feel good about it.
And the time to do the work wasn’t much. Writing it up, my garsh that takes a while, but those few tests and few lines of code, hardly any time at all. I’m pretty confident that if I were always this careful, I wouldn’t slow down much, if at all, and I’d be really very confident that my code was doing what I intended it to do.
Now, in real life, as I’m sure you’ll see in subsequent articles, I do take bigger bites. I get impatient. I am uncertain how to test something. I leave cracks where maybe there should be no cracks. And you’ll see me get in trouble for doing it.
I’d work this way all the time, if I could, because this is my best work. And, I am human, and sometimes I can’t give as much as I can at other times. I forgive my humanity, but I admit that sometimes I mumble EXPLETIVE DELETED, Ron!@4#@!
Then I forgive myself again, and try again.
Ever tried. Ever failed. No matter. Try Again. Fail again. Fail better. –Samuel Beckett
Enough for today, I’ve got reading to do. See you next time, fellow robots and friends of robots!