Ridiculously Simple
Bryan comes up with a perfect term for what we’re trying to do. We simplify one bit, improve another, must about others.
While I waited for Brian (GeePaw couldn’t make it) I came up with these topics:
- Use the forward-looking logic of
looking
state (take) inladen
state (drop): will simplify Bot and is more consistent; - Change World so that Bot cannot walk over a Block;
- Discuss and possibly deal with how moving works, which seems incompatible with client-server.
I’m sure there will be lots more. And I’m going to switch right now to notes on paper, which I think I can do while paying attention to the team and screen.
Here’s what a bot does now (after this morning’s work):
- Sometimes it is tired, and it just wanders for a few moves;
- Otherwise it is either “looking” for a Block, or “laden” with aa Block;
- When it is looking, it wanders until it comes up on a block that has a space on at least one side of it,
-B-
,-BB
, orBB-
. It picks up the block, is “laden” and tired. So it wanders for a bit … - And when laden, it wanders until it comes up on an empty space with a block on at least one side of it,
B--
, or--B
, orB-B
. Then it drops its block adjacent to the one on the ground. Then it is tired again.
The “tired” feature causes the Bot to tend to wander away from where it just dropped or picked up, so that it is not likely to just pick up the same block that it just put down.
So the bot’s entire behavior is just this:
def do_something(self):
self.state()
self.move()
def walking(self):
if self.tired <= 0:
self.state = self.looking
def looking(self):
if self.can_take():
self.take()
if self.inventory:
self.tired = 5
self.state = self.laden
def laden(self):
if self.tired <= 0:
if self.can_drop():
block = self.inventory[0]
self.world.drop_forward(self, block)
if block not in self.inventory:
self.tired = 5
self.state = self.walking
Naturally, there’s can_take
and can_drop
to be shown, but they, too, are simple:
def can_take(self):
return self.forward_name() == 'B' and (self.forward_left_name() == '_' or self.forward_right_name() == '_')
def can_drop(self):
return self.forward_name() == '_' and (self.forward_left_name() == 'B' or self.forward_right_name() == 'B')
def forward_name(self):
forward = self.location.forward(self.direction)
return self.vision.name_at(forward)
def forward_left_name(self):
forward_left = self.location.forward_left(self.direction)
return self.vision.name_at(forward_left)
def forward_right_name(self):
forward_right = self.location.forward_right(self.direction)
return self.vision.name_at(forward_right)
Looking at that now, I suspect that the forward_...
methods might belong somewhere else, perhaps on vision
, but we can’t see everything all the time.
We added some tests, and we removed my fancy pattern stuff that let me draw little pictures of the pattern the Bot sees. Our bots now can really only sense forward.
Then we changed the world so that a bot cannot step on another bot or block:
class Map:
def attempt_move(self, id, location: Location):
entity = self.contents[id]
if self.location_is_valid(location):
entity.location = location
def location_is_valid(self, location: Location) -> bool:
if self.is_occupied(location):
return False
return self.is_within_map(location)
def is_within_map(self, location):
return 0 <= location.x <= self.width and 0 <= location.y <= self.height
def is_occupied(self, location):
return self.entity_at(location.x, location.y)
This just means that Bots bounce off other Bots or Blocks, instead of walking over them. Made sense to us at the time.
The method entity_at
returns None if there is no entity at that location. I suspect it should be refactored to accept a location and decode it internally. Might be more consistent. I think I’d like to recast location_is_valid
as well, down to a single return
statement.
Discussion
We talked about ships and shoes and sealing wax, but not cabbages. We wonder whether we could implement some kind of evolution, and how we could derive some kind of cooperative behavior. No decisions, but …
Ridiculously Simple
At some point as we worked, Brian used the phrase “ridiculously simple”, describing how our Bot works, and I think it is exactly what we’re going for. The Bots exhibit interesting group behavior: they run around picking up blocks and putting them in piles. Yet their behavior is little more than “see a block, pick it up”, “see a block, put yours down”. They have no idea that they are creating clumps.
We love this! We’re wondering what else we can do with “ridiculously simple” rules like this. What if they could be “hungry”, not just tired, and there was Food in the work, not just Blocks. If too hungry, maybe you drop your Block wherever you are and seek Food. Maybe if you’re very hungry, you eat the Food, but if you’re only sort of hungry you start picking up Food and stacking it like you stack Blocks.
We’re wondering about pheromone paths and how a Bot might somehow “learn” but in a Ridiculously Simple way, or how they might evolve in a Ridiculously Simple fashion.
Real World
In the real world of programming, we are not likely to build something that has the business behavior we want by virtue of pheromones or a tendency to pick up blocks if you don’t have one and to put one down if you have. But we can, I want to offer, always benefit from building our programs out of truly simple ideas, simple objects, simple code.
So we want this particular kind of Bot to serve as a beacon, a poster child for simplicity. And, equally important, perhaps more important … they’re really fun!
See you next time!