Biots Spike
A tiny experiment in Python on my iPad has inspired me to try a spike this morning.
Last night, I wrote a tiny Python program that did a typewriter-drawing kind of thing on the console, repeatedly clearing and redrawing, so that the output looks like a letter R moving around on the screen. R for biot, obviously. This morning I want to work on something like this:
- In a small world1, place a “block supply”, an object where you can get any number of blocks;
- If the Biot is adjacent to the block supply, it can say “take”, perhaps naming a direction;
- The Biot will then have a block in some kind of inventory;
- It can go somewhere else and say “drop”, and put the block down.
It should be clear that with a series of moves, the Biot can build a wall of blocks. This demonstration, if we can do it, would go a long way toward blowing the minds of the FGNO Zoom meeting this coming Tuesday night.
Bryan has a life, and is probably not available to pair today. We have agreed that we can spike, and connect up later. We’d both prefer to pair, but, well, I kind of brought the spike topic up because I knew I’d be wanting to program at 0700 on Saturday mornings and the like, because that’s how I keep the demons away. So here we are.
The current state of the program is rather far from being able to do that. We have a world, and a Biot. The Biot has a location in the World and it can move dx, dy as it desires. There are no other things in the world, and no interaction of any kind.
In addition, I don’t think it’s fair for me to come up with any really decent solutions alone. It will create a gap in understanding between me and the other team members, Bryan. But as a demo … it would look great.
What if the spike works like this: I build a dummy drop
command that places a ‘B’ in the map, B for Block. With this in place, a simple sequence of move / drop would build a wall. If we place another B somewhere near by, saying that it is a Block Supply, a more complex series of move / (take) / move / drop, will seem to be building a wall by grabbing blocks and lining them up. Or … maybe we just have lots of blocks lying around and we arrange them in a row. I don’t know, we’re just trying to think of something simple we could do.
I build a dummy take
command, time permitting, that somehow signals that you have a Block. Etc. Too much speculation, let’s do something.
Let’s see … I think I’d like to start by drawing the current situation on the console.
def test_draw_empty_world(self):
line = '__________\n'
expected = ''
for row in range(10):
expected += line
world = World()
drawing = world.draw()
assert drawing == expected
I’m assuming that the world is 10x10 for our purposes here. We need draw
:
def draw(self):
result = ''
for col in range(10):
for row in range(10):
result += '_'
result += '\n'
return result
Test is green. I need to decide what to do. We haven’t worked out a real protocol, but I think I said I wouldn’t push a spike if I did one. And we don’t like branches and, anyway, as a home user of all this, I never use them so don’t know how to behave well if I did create one. I suppose it would be harmless, though.
I’ll try it, worst case I break everything. New branch. It asks me to check it out, I do, my code is still here, does this mean I can commit? I seem to be on a branch, drawn on top of origin master. We forgot to change the name, sorry.
Now I’d like to at least show the current Biot on the drawn map. A new test.
def test_draw_world_with_biot(self):
world = World()
biot = Biot()
world.add(biot)
world.move(biot, -5, -5)
drawing = world.draw()
assert drawing == ''
Now in draw
, instead of always drawing an underbar, the World should check to see if there is anything at the given location and draw that instead. I see an issue, which is that in our drawing we have 0,0 at top left and 10,10 at bottom right. Maybe that’s OK, let’s just go with it. The draw
method would like to say this:
def draw(self):
result = ''
for col in range(10):
for row in range(10):
result += self.symbol_at(col, row)
result += '\n'
return result
Programming by Wishful Thinking. First version of symbol_at
is obvious:
def symbol_at(self, col, row):
return '_'
Our first draw test passes, and the second fails because we are expecting an empty string. I propose to solve this problem by approving what comes out and then figuring out how to represent it in the test. Creating expected reports is always a pain.
def symbol_at(self, col, row):
possible_item = self.biots.at_location(col, row)
if possible_item:
return 'R'
else:
return '_'
The self.biots
is an instance of Entities. For now, I plan to put everything in there, including blocks. Right now there are only biots. I propose a new method on Entities that searches and returns things by location.
~~~pythonclass Entities: def init(self): self.contents = {}
def place(self, biot):
self.contents[biot.id] = biot ~~~
Well then, how about this:
def at_location(self, x, y):
point = Point(x, y)
for item in self.contents.values():
if item.location == point:
return item
return None
I want to see the failure now.
('__________\n'
'__________\n'
'__________\n'
'__________\n'
'__________\n'
'_____R____\n'
'__________\n'
'__________\n'
'__________\n'
'__________\n') != ''
Perfect! I note that Python has this really odd feature about appending strings literals. Can I just do this:
def test_draw_world_with_biot(self):
expected = \
'__________\n' \
'__________\n' \
'__________\n' \
'__________\n' \
'__________\n' \
'_____R____\n' \
'__________\n' \
'__________\n' \
'__________\n' \
'__________\n'
world = World()
biot = Biot()
world.add(biot)
world.move(biot, -5, -5)
drawing = world.draw()
assert drawing == expected
I can. Green. Commit: can show biot location.
Now what? I need to have some other kind of thing in the world, or at least to seem to have. I think that the Entities collection biots
may want to contain all the things in the world, at least for now. But there are no other things at this time.
I believe we are moving in the direction of an Entity superclass (or just notion, duck typing after all) that represents the common aspects of being a Biot, a Gold Mine, a Block Supply, or a Food Wagon in the World. For now … by way of hackery, let’s just make new Biots with different names. Our original will be named ‘R’ for Biot. (I’m saving B for Block and Block Supply).
class Biot:
def __init__(self, name='R'):
self.id = None
self.location = Point(0, 0)
self.name = name
I just added the name stuff. Now a new test with a new Biot / Block supply:
def test_draw_world_with_second_entity(self):
expected = \
'__________\n' \
'__________\n' \
'__________\n' \
'__________\n' \
'__________\n' \
'_____R_B__\n' \
'__________\n' \
'__________\n' \
'__________\n' \
'__________\n'
world = World()
biot = Biot()
world.add(biot)
world.move(biot, -5, -5)
block = Biot('B')
world.add(block)
world.move(block, -2, -5)
drawing = world.draw()
assert drawing == expected
Let’s see the failure:
('__________\n'
'__________\n'
'__________\n'
'__________\n'
'__________\n'
'__________\n'
'__________\n'
'__________\n'
'_____R____\n'
'__________\n') != ('__________\n'
'__________\n'
'__________\n'
'__________\n'
'__________\n'
'_____R_B__\n'
'__________\n'
'__________\n'
'__________\n'
'__________\n')
I am surprised. Expected to see two R’s there but only got the one. Oh. Both Biots got the same ID. That won’t do.
id = 100
def __init__(self):
self.biots = Entities()
def add(self, biot):
location = Point(10, 10)
biot.id = self.next_id()
biot.location = location
self.biots.place(biot)
def next_id(self):
World.id += 1
return World.id
Now the fail, I hope, has two ‘R’ entries.
('__________\n'
'__________\n'
'__________\n'
'__________\n'
'__________\n'
'_____R____\n'
'__________\n'
'__________\n'
'_____R____\n'
'__________\n') != ('__________\n'
'__________\n'
'__________\n'
'__________\n'
'__________\n'
'_____R_B__\n'
'__________\n'
'__________\n'
'__________\n'
'__________\n')
If Hill ever sees this, he will mock me mercilessly. This is the classic confusion of x, y vs row, column. Hill has some how memorized a way that he always does this stuff so that he never makes a mistake of this kind—or so he says. What I have done is reverse column and row in my draw. This will change things a bit, might break more, but not in a bad way.
def draw(self):
result = ''
for row in range(10):
for col in range(10):
result += self.symbol_at(col, row)
result += '\n'
return result
And now:
('__________\n'
'__________\n'
'__________\n'
'__________\n'
'__________\n'
'_____R__R_\n'
'__________\n'
'__________\n'
'__________\n'
'__________\n') != ('__________\n'
'__________\n'
'__________\n'
'__________\n'
'__________\n'
'_____R_B__\n'
'__________\n'
'__________\n'
'__________\n'
'__________\n')
Perfect!
Well, not quite, I seem to have counted incorrectly somewhere. Yes, I did, I said -2 and entered -3. And, of course, we need to use the names.
Ah. Part of the issue is that we start our biots at 10, 10, which is actually outside the print range. Anyway:
def symbol_at(self, col, row):
possible_item = self.biots.at_location(col, row)
if possible_item:
return possible_item.name
else:
return '_'
And the tests pass. Green. Commit: Biots have single character names. World draw
returns 10x10 typewriter picture of the world’s biots at their locations.
I think that’s enough for one session alone and certainly enough for one article.
__________
__________
__________
__________
__________
_____R__B_
__________
__________
__________
__________
Summary
A small spike that “just” draws the biots in the world with a silly typewriter picture. But I want to suggest that it actually represents a lot more than that. We now have useful inklings about at least these topics:
- How might we represent more than one thing in the world? (We’ve done it.)
- How could we display the world in an arcade style format? (Just like this, just better pictures of the cells.)
- How might we quickly represent motion without a lot of game work? (Draw lots of typewriter pictures.)
- How can we find out what is in a given location. (Solved, albeit terribly inefficiently.)
In essence, with this spike, we have put tiny cracks in the hard rocks of the problems we face. And once we have a crack in the rock, it is easy to split it and to work with it.
I chose to start with the draw
for two reasons. First, it does represent the end point for a demo that I’d like to give. But also, it seemed to me that it would not require me to make many new objects or to make extensive changes to the objects we have. So this spike wouldn’t be likely to leave Bryan, who could not be here, in the dust, wondering what happened.
But because the draw
required at least two items in the world, we kind of stumbled into Biot names, and I think we’re on the edge of there being different types of things, so long as they accept the standard simple protocol we’re using now, of id, location, and name. We sketched in giving different ids to each Biot, though I suspect that for security purposes we probably won’t want sequential integers in the long run. We have a very inefficient way of knowing what is at any given location.
There’s probably more. A simple spike like this can illuminate so many of our problems, and can encourage us to find simple solutions where we might be inclined to build something more complicated than is actually needed. I prefer to start ridiculously simple, if I can, and evolve to more complicated solutions as need arises.
See you next time!
-
After all. Remind me to tell you of the traumatic experience I had when the Small World ride at Disney broke down and we had to wait to be rescued, with That Song playing over and over, sung by tiny horrid doll creatures. I have never been the same since. ↩