BOTS Update
Some pairing has been taking place, without contemporaneous writing. I will explain what we have. No, it is too much. I will sum up. Includes remarks on working without a
netspecification.
Since the previous article (on the 19th), Bryan and I have paired a few times, and made some progress both in what the program does, and in our understanding of what we’re trying to create.
Evolving the “Spec”
Yes, we are really working without a real “specification” of what is to be done. To some readers, working without a concrete specification of what is to be done may seem entirely wrong. To me, and my close colleagues, it seems almost exactly right.
Recall that in the Agile Manifesto we said:
Business people and developers must work together daily throughout the project.
We really meant that. We meant that we believe that the best results come from putting the people with the need together with the people who can build things, and creating the understanding of what is to be built simultaneously with building it. So Bryan and I, and soon, we hope, more team members, are working with just a vague understanding of what we’re building. This seems like a convenient time for me to say a bit about our current thinking.
We’re building a game. The purpose of building it includes getting a game, but that is really a low priority for us. We intend to build the game as publicly as we can manage, using the practices and tools that we recommend as coaches and authors, and publishing as much as we can about what we’re doing, including the code, so that people who want to learn how we do things can read our code, our articles, and perhaps some videos.
What is the game?
The game concept varies pretty wildly. What is more or less fixed is that the game will present a world, and in the world the player can place robots (formerly biots). Robots have various kinds of behavior, and the overall purpose of the game is to explore the world, and to create value to keep your robots going. We are no sure at all what kind of value to create. We do not want the robots shooting and killing each other, nor do we want them to be shooting and killing the local indigenous creatures if there are any. We don’t even want them to be seriously damaging the environment by digging up gold and drilling for oil.
At this writing, I am mot sure how to create an interesting game involving robots who just want to live in harmony with nature. I am hoping to find out.
What can bots do?
Right now, we are trying to create bots with very simple behaviors, representing having nothing on their agenda, having curiosity about something, and some simple desire that will represent not running out of fuel or something. We have one particular thing in mind. It goes like this:
- When the bot encounters a block, if it does not have a block, it picks up the block.
- When the bot encounters a place that needs a block, if it has a block, it puts its block there.
- Otherwise the bot wanders around. If it seems something in the distance, curiosity causes it to look at it more closely.
I was originally thinking that the game player would be able to, say, mark an area that needed a bridge with a series of “needs block” markers, and the bots would begin to grab blocks and put them down to build the bridge.
In this week’s Zoom, Chet mentioned a simulation, whose source we do not recall, where ants had very simple behavior: if you see a grain of something good, pick it up. If you have a grain of something good and encounter another grain, put yours down.
If we recall the story, rules roughly like those will cause the ants to convert an area with lots of separate grains into an area with several piles of grains. So, my thinking today, as yet without any advice from Bryan, is that that might be almost simple enough that we could do it.
How are we programming?
We’ll review some code in a moment. In general, we try to write tests for every bit of code that we produce, and we’re doing pretty well at it. We’ve mostly been concentrating on the World, with some supporting objects for it, and until a day or two ago, the Bot had almost no behavior. If you want to review all the code, there is a link above to the repo. Let’s look at a few objects and their tests.
Because it’s on the screen, we’ll start with Bot. As I describe things, you’ll notice that there are some things that are hardly used, or even not used at all. Those arise when we say something like “how will we store objects in the world?”, or “how will the bot know what is in the world?”. We write a little test and a little code. Some of that code may be thrown away. Perhaps most of it will be. The point of writing it was to change our own state from “we don’t know how to do this” to “we know at least one way to do this”. The purpose is to build up just enough understanding so that we can say “yeah there are decent ways to solve that”, which lets us move on to more interesting matters.
We’ll look at Bot in segments. At the top:
class Bot:
def __init__(self, x, y):
self.world = None
self.id = None
self.name = 'R'
self.location = Point(x, y)
self.direction = Direction.EAST
self.direction_change_chance = 0.2
@property
def x(self):
return self.location.x
@property
def y(self):
return self.location.y
Not much to see here. It has a pointer to the world, so that it can do things. It has a location, a starting direction … and direction_change_chance
. We’ll see the details in a moment, but this is the chance that, well, the robot will change direction.
class Bot:
def scan(self):
return self.world.scan(self)
def is_close_enough(self, entity):
return entity.location.distance(self.location) < 10
The method scan
is essentially unused, but is something we wrote to get comfortable with how we could get information about the world to the bot. The method is_close_enough
is used in World’s scan
to decide whether to show an object to the bot during a scan. Essentially that 10 represents the range of the bot’s scanner. If you were to ask me why we didn’t just return the number, or give the world the bot’s abilities list, I would have no answer. This is what we did.
Now for something almost interesting:
We have built a PyGame viewer. I say “we” Bryan basically built it while I watched, yesterday. the viewer’s main loop looks like this:
def on_execute(self):
if self.on_init() is False:
self._running = False
while self._running:
pygame.time.delay(100)
for event in pygame.event.get():
self.on_event(event)
self.clear_screen()
self.draw_grid()
self.bot.do_something()
self.draw_world()
pygame.display.update()
self.on_cleanup()
In the PyGame, which I’ll show you in a moment, we populate with a block and a bot, and every tenth of a second delay(100)
, we tell the bot to do_something
. What does it do?
class Bot:
def do_something(self):
# first real behavior is to randomly wander until you see something
# then move to it
old_location = self.location
if random.random() < self.direction_change_chance:
self.change_direction()
self.step_in_direction()
if self.location == old_location:
self.change_direction()
self.step_in_direction()
def step_in_direction(self):
d = self.direction
if d == Direction.NORTH:
self.world.move_north(self)
elif d == Direction.EAST:
self.world.move_east(self)
elif d == Direction.SOUTH:
self.world.move_south(self)
elif d == Direction.WEST:
self.world.move_west(self)
def change_direction(self):
direction = self.direction
while direction == self.direction:
direction = random.choice(list(Direction))
self.direction = direction
In do_something
, we save our old location. We roll the dice to see if we should change direction. Remember that chance, 0.2? There it is. So we might change direction. Then we step. We are not guaranteed to move, but upon stepping, our location will be updated by World so we check to see if we actually moved. If we didn’t, we change direction (and move again, which I think is a bug. We should wait until next do_something
, having consumed our turn with the first move.)
We change direction by picking a random direction until we get one that isn’t our current direction. We move in our chosen direction by asking the world to move us.
With this in place, our game looks like this:
Perhaps you can imagine the joy we felt when we first saw that motion on the screen. It just seemed to make all our work so far, well, more visible, ore tangible. It was pleasing.
It will also be a terrible temptation, because it may often seem to us that testing something explicitly is difficult, we can just run it and see if it works. I believe, and my colleagues believe, that that’s an inferior way to work, but it is ever so tempting. You are invited to watch and see what happens.
Friday
Suddenly it is Friday. There has been yet another programming session, and I’m that much further behind on keeping you up to date. And I simply cannot do what I generally do, which is to share with you the fumbling detailed thoughts as I go, because in the team situation, things move too fast and I can’t write and team at the same time. Even as poorly as I team, and believe me, after a decade of almost always programming alone, I suck at teaming.
I’ll do my best to take notes and keep you up to date as well as I can. Maybe things will even get better, with less mumbling and more directed commentary.
With that in place, what did we do yesterday?
Yesterday, we did a bit of cleanup refactoring and then Bryan took the keyboard and worked on picking up a block.
We added three new tests:
def test_bot_notices_a_block(self):
world = World(10, 10)
bot = Bot(5, 5)
world.add(bot)
block = Block(6, 5)
world.add(block)
bot.do_something()
assert bot.has(block)
def test_take_a_block(self):
world = World(10, 10)
bot = Bot(5, 5)
world.add(bot)
block = Block(6, 5)
world.add(block)
world.take_east(bot)
result = bot.scan()
expected_scan = [('R', 5, 5)]
assert result == expected_scan
def test_bot_gets_a_block(self):
world = World(10, 10)
bot = Bot(5, 5)
world.add(bot)
block = Block(6, 5)
world.add(block)
world.take_east(bot)
assert bot.has(block)
One at a time, of course. The first test got changed along the way, because we enhanced do_something
to do the take
as part of this effort.
We modified Bot:
class Bot:
def __init__(self, x, y):
self.world = None
self.id = None
self.name = 'R'
self.location = Point(x, y)
self.direction = Direction.EAST
self.direction_change_chance = 0.2
self.inventory = [] # <===
def has(self, entity):
return entity in self.inventory
def receive(self, entity):
self.inventory.append(entity)
def pick_up_block(self):
result = self.scan()
if self.block_to_east(result):
self.world.take_east(self)
def block_to_east(self, result):
return True
def do_something(self):
self.pick_up_block() # <=== added
old_location = self.location
if random.random() < self.direction_change_chance:
self.change_direction()
self.step_in_direction()
if self.location == old_location:
self.change_direction()
So before we move, we look around to see if there is a block (to our east, it’s early days here), and if there is we tell the world take_east
and as we’ll see in a moment that puts the block into our inventory, so that our test for bot.has(block)
will pass.
- My Mistake
- This implementation is about as minimal as we could make it. I was really pushing for more, because I had a slightly larger idea or just wanted to take a bigger step, and in the process, I pushed too hard and rather ticked Bryan off. I have apologized and hope he’ll forgive me.
-
In my defense: no defense, I screwed up.
OK, what else is really worth looking at?
class Entities:
def remove(self, id):
del self.contents[id]
We needed to remove the block from the world, I guess.
class Point:
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
This isn’t type correct, really you add a vector to a point to get a new point but let’s get serious, this will do for now. We use that to find the point to our east … well I thought we did. I see that our code currently just assumes a block to east. I think we stopped before we got around to actually checking the scan.
class World:
def take_east(self, bot: Bot):
location = bot.location + Point(1, 0)
entity = self.map.entity_at(location.x, location.y)
if entity:
self.map.remove(entity.id)
bot.receive(entity)
Here, the world implements take_east
, using the Point add, and actually looking to see if there is something there and sending it to bot.receive
if it is. The bot will put it in inventory if it gets the receive.
So as things stand, the bot tries take_east
on every move, because, right now, it thinks there is a block_to_east
though it is usually mistaken.
I think you’re pretty much up to date. Here’s what I notice:
Summary
- Teamwork
-
I am terrible at teamwork. I vow to improve and have asked my friends to advise, cajole, and pummel me as needed.
- Changing the Spec
- We change the spec every day, often more than once per day. We change it in the small, picking something to work on that wasn’t i our plan yesterday or an hour ago. And we change it in the large as we begin to see how it will feel to play and as we get a better picture of how to create a game where collaboration is the way to win rather than destroying either the other players or the planet.
-
I was taught otherwise in my youth o! So Long Ago. I was taught that you shouldn’t start programming until you knew what you were supposed to program and that you needed a solid spec and that you should probably have a serious and draconian change control process.
-
That is exactly what we do not do. We work together, imagining the new product and building it as we go. We have learned ways of making software that make this feasible. The processes, practices, and tools of today allow us to build tiny products and grow them smoothly toward more and more capability.
- Small Steps
- Our current block-taking capability is phenomenally thin. The bot always thinks there is a block to his east and tries to take it. It does not consider any other direction. The bot’s inventory is a simple list, and … ooo, there may be a bug. A design error. Maybe. When there is something (an entity) to the east, the World sends it to
bot.receive
. Can we send an entity over the phone line? -
I guess we can. We can surely encode it in JSON and decode it. It is no more than a sequence of (name, x ,y). OK, stand down, emergency over.
-
We try to take small steps. Even when I push for a larger step (dammit), it’s still a small step. We build as limited, as fragile, a solution as we can. I think of it as firing a rope across the chasm on an arrow, and the folks on the other side tie the rope to a tree and look, we have the beginnings of a bridge.
-
If you don’t mind some NSFW language, look up “how to draw an owl”. We strive to have the two circles, and over time we fill in the rest of the blasted owl.
- Guided by Tests
- We try to write little tests for everything. When the tests are hard to write, we try, oh my g*d do we try, to write them anyway and make them easier to write. We know they help us. And sometimes, some of us (only me? OK …) really want to write the code before the test. We’re sort of allowed to do that, though we are supposed to throw the code away.
- But not by Rules
- The way we work is adaptive, based on long years of experience, and on the things others have taught us, and the random life lessons we’ve picked up along the way. We are strongly inclined to stay within our usual guidelines and yet we do not hesitate to step outside them when the fancy strikes. We try always to say “hold my beer”, we expect that we’ll often crash and burn, and that we’ll learn something along the way.
- With Fun and Joy
- We do this for the fun and joy of it. Sometimes, like yesterday, it’s not joyful. I accept the responsibility for yesterday, and vow to do better next time.
See you then!