Grouper
No, not the fish. An idea about flavors of Blocks. Exploration, Spike, Rollback. Just fine.
I’m not sure where this idea came from. Possibly because I was thinking about the Bots needing food. Possibly because my wife’s friend told her that a nearby restaurant was serving grouper, a uniquely unattractive fish. OK but anyway, our Bots pick up “blocks” and when they are carrying a block and find another one, and if the area smells enough like blocks, they put theirs down. That behavior is enough to cause the Bots to arrange randomly-placed blocks into groups.
So what if there were different block scents, or flavors, and the Bots placed theirs near places that smelled the same? Wouldn’t they automatically wind up grouping the blocks by flavor?
I mean to find out.
Planning / Exploration
Despite that I’ll be coding in a very few minutes, I really do recommend a bit of planning before diving into something new. We may well find that the plan isn’t what we do, and that’s OK. But, as the song says, you gotta know the territory.
- Scent
- The World calculates the “scent” of the cells near each Bot and provides that integer value on each cycle. We’ll probably have multiple scents now, so the notion of “scent” will surely need to be expanded into some kind of collection of scents. This area smells like roses, coffee, and a whiff of what seems like Penne all’Arrabbiata, sort of thing.
-
But … see below …
- World vs Bot
- We’re using the same classes for Bot, and, I think, Block, on both sides of the program. This might be a good time to make the separation more explicit.
- Block Types
- We distinguish Bot from Block by name. It is almost certainly time to move toward some notion of entity type, and that notion needs to be somewhat shared, so that the Bot side can check to see if the local scent smells like whatever it’s carrying. (Or does it need to do that? Probably if you’re carrying a clove of garlic, all you can really smell is garlic. A weaker notion of scent might suffice, such as computing scent to be the cumulative scent that’s the same as the scent of the thing you’re carrying.)
Exploration / Planning
Let’s look at some code. We’ll start with how scent is computed.
class World:
def step(self, bot: Bot):
self.map.attempt_move(bot.id, bot.forward_location()) # changes world version
self.set_bot_vision(bot)
self.set_bot_scent(bot)
def set_bot_scent(self, bot):
bot.scent = self.map.scent_at(bot.location)
class Map:
def scent_at(self, location):
total_scent = 0
for dx in (-2, -1, 0, 1, 2):
for dy in (-2, -1, 0, 1, 2):
current = location + Direction(dx, dy)
scent = self.relative_scent(location, current)
total_scent += scent
return total_scent
def relative_scent(self, location, current):
found = self.at_xy(current.x, current.y)
scent = 0
if found and found.name == 'B':
scent = 4 - location.distance(current)
if scent < 0:
scent = 0
return scent
One thing we notice early on is that we are checking the name of the things found, because that’s our only notion of the type of the entity found. Now, if we want to go with the single-scent idea, we could pass down the name to check, or the type to check if we had type. If, however, we want to assess all the various scents, we’d be building a structure here, perhaps with relative_scent
returning a value and a scent type, which we would duly accumulate into a dictionary or something.
I am of course anxious to get this new feature in place, because I think it will be so fine to watch these unintelligent little bots grouping things. Creating a more complicated form of scent seems like it will delay producing a result. But it will also complicate what is already not a very robust structure, the entity type. It seems to me that we “should” enhance our notion of entity type on the world side, and that we “probably should” commit to a more complicated scent structure.
It would be nice if these two ideas weren’t mutually contingent in any horrid way.
Let’s see what the Map is really storing.
class Map:
def place_at(self, entity, drop_location):
if self.location_is_open(drop_location):
entity.location = drop_location
self.place(entity)
return True
else:
return False
def place(self, entity):
self.contents[entity.id] = entity
class World:
def drop_forward(self, bot, entity):
if self.map.place_at(entity, bot.forward_location()):
bot.remove(entity)
def add(self, entity):
entity.world = self
World.next_id += 1
entity.id = World.next_id
self.map.place(entity)
The latter method is used when we add a Block. The Map assumes that an entity has a location
and when we place
we do not override that value, while place_at
does set the location as directed.
Let’s dig a bit further on this, see where World gets these entities its placing:
class World:
def add_bot(self, x, y, direction = Direction.EAST):
id = self.add_world_bot(x, y, direction)
returned_bot = Bot(x, y, direction)
returned_bot.id = id
returned_bot.world = self
return returned_bot
def add_world_bot(self, x, y, direction = Direction.EAST):
bot = Bot(x, y, direction)
self.add(bot)
return bot.id
Here we create an instance of Bot. I think that class should really belong to the client side, and we should have a new thing, a WorldEntity, that we use on the world side. And the adding of a block should have a method corresponding to add_bot
.
The method add
sets the id
. Its name does not make that clear. Should it perhaps be made more clear? add_with_id
perhaps.
Musing
I can see some issues here. If we do create a new class, maybe WorldEntity, we can, and perhaps should ensure that only WorldEntities go into the Map, more for safety against cross-contamination with the client side than any other reason.
What about the DirectConnection? Does it need to be aware of this kind of change? A quick look tells me that DC is pretty good about passing only the id
and letting World deal with it. So that’s a good thing.
Spike
The exploration so far tells me that this may turn out to be a bit tricky, if we want to take the occasion to get a new WorldEntity kind of thing built on the World side, and I think we do want that. I do not believe we can just ease this in, at least not as I understand things now.
I want to experiment with a WorldEntity class, with a real type
, just to get a sense of how this might go. Because I plan to throw it away, but because I may want to have a few save points, I’m not sure how to proceed. I think we’ll create the new file: we can be sure we’re going to have some kind of WorldEntity.
I do not promise to throw this away, but I will if it deserves it. This sequence seems ripe for this idea:
def add_bot(self, x, y, direction = Direction.EAST):
id = self.add_world_bot(x, y, direction)
returned_bot = Bot(x, y, direction)
returned_bot.id = id
returned_bot.world = self
return returned_bot
def add_world_bot(self, x, y, direction = Direction.EAST):
bot = Bot(x, y, direction)
self.add(bot)
return bot.id
def add(self, entity):
entity.world = self
World.next_id += 1
entity.id = World.next_id
self.map.place(entity)
There in add_world_bot
… we could just add a world bot! I think I’ll assume a construction method.
def add_world_bot(self, x, y, direction = Direction.EAST):
bot = WorldEntity.bot(x, y, direction)
self.add(bot)
return bot.id
We seem to have no such class. Just to try to get started:
class WorldEntity:
def __init__(self, type, x, y, direction):
self.type = type
self.location = Location(x, y)
self.direction = direction
@classmethod
def bot(cls, x, y, direction):
return cls('B', x, y, direction)
I’ll deal with type soon, if this pans out. 13 tests are breaking. That seems unlucky somehow. But I’m not surprised, this thing has no behavior.
It appears that we are returning a WorldEntity in this test:
def test_wandering(self):
world = World(10, 10)
client_bot = world.add_bot(5, 5)
client_bot.do_something()
world.update_client_for_test(client_bot)
loc = client_bot.location
assert loc != Location(5, 5)
Well, I notice that we’re going after the id
in the adding code, so I’ll add that, but I don’t see how it is that we could return a WorldEntity … unless we are returning something from the map??
class WorldEntity:
def __init__(self, entity_type, x, y, direction):
self.id = None
self.world = None
self.type = entity_type
self.location = Location(x, y)
self.direction = direction
@classmethod
def bot(cls, x, y, direction):
return cls('B', x, y, direction)
I added in the world
member. I’m not even sure we use it but the code is setting it.
Enhance the tests to be sure that we’re returning a client bot, and we are:
def test_wandering(self):
world = World(10, 10)
client_bot = world.add_bot(5, 5)
assert isinstance(client_bot, Bot)
client_bot.do_something()
world.update_client_for_test(client_bot)
loc = client_bot.location
assert loc != Location(5, 5)
Something has gone weird inside do_something
.
def fetch(self, entity_id):
> return self.map.at_id(entity_id)._knowledge.as_dictionary()
E AttributeError: 'WorldEntity' object has no attribute '_knowledge'
Oh, right. I kind of wish I had looked more carefully. Of course our Entity needs _knowledge
. (We’re adding in a lot of elements that only Bot entities need, but that won’t trouble us now. We can deal with a bit of waste now, add polymorphism later, if this works at all.)
class WorldEntity:
def __init__(self, entity_type, x, y, direction):
self.id = None
self.world = None
self.type = entity_type
self.location = Location(x, y)
self.direction = direction
self._knowledge = Knowledge(self.location, direction)
I copied that from the Bot. One of my tests passed, down to 12 broken now.
I realize now that the Bot is rigged to keep lots of information in its knowledge member. We may or may not do that here. I do expect to run up against fetch
very soon here. It’s starting to look more and more like rolling back, taking some understanding with us but no code. But not yet.
WorldEntity does not understand forward_location
. I can agree.
ENhancing the test:
def test_wandering(self):
world = World(10, 10)
client_bot = world.add_bot(5, 5)
assert isinstance(client_bot, Bot)
assert client_bot.id is not None
client_bot.do_something()
assert client_bot.id is not None
world.update_client_for_test(client_bot)
loc = client_bot.location
assert loc != Location(5, 5)
I find that after do_something
, the client bot id is None. Curious.
OK, roll back and report what we’ve learned.
Things really went bad in World.fetch
:
def fetch(self, entity_id):
return self.map.at_id(entity_id)._knowledge.as_dictionary()
Our WorldEntity wasn’t really using the _knowledge
member, so when we convert it to a dictionary and use that to update the Bot side, bad things happen, including setting the id
, which is None in the _knowledge
.
Demeter feels violated!
The code for fetch
violates the Law of Demeter, which basically says you should keep your hands to yourself, and not go reaching into the pockets of other objects. Applying the law here, we’d probably have something like this:
def fetch(self, entity_id):
return self.map.at_id(entity_id).fetch()
And then whatever object returns from the map does the work. Or we might even say:
def fetch(self, entity_id):
return self.map.fetch(entity_id)
I think that’s less desirable than sending a message to the object in the map. The map doesn’t know much about things that it holds, mostly just how to get them by id
and by x and y. And it can try to move them, because it is the manage of location.
The WorldEntity, before we rolled back, looked like this:
class WorldEntity:
def __init__(self, entity_type, x, y, direction):
self.id = None
self.world = None
self.type = entity_type
self.location = Location(x, y)
self.direction = direction
self._knowledge = Knowledge(self.location, direction)
@classmethod
def bot(cls, x, y, direction):
return cls('B', x, y, direction)
def forward_location(self):
return self.location + self.direction
@property
def name(self):
return self.type
@property
def x(self):
return self.location.x
@property
def y(self):
return self.location.y
We needed those methods along the way of working out some of the issues with the tests failing.
Summary
So, we’ve explored and experimented. Enough for now, but what’s the plan?
There is no plan. I’ll let this experience sink in a bit, and maybe muse a bit as the day goes on, but the actual plan will be formed in the next session … and then, of course, we may follow it or we may not.
Tentatively, I see two possible paths. One would be to create a new WorldEntity class along this morning’s lines, with a little TDD to get it in shape prior to plugging it in. Another approach, which I will probably not try, would be to copy the Bot class to a new class WorldEntity and then pare it down to the minimum. In favor of that idea is that it would probably work. Against, it’s likely that we’d miss some cruft, and the object would be shaped more the way the Bot side would want, not the World side.
It might make an interesting experiment, however.
Whatever we try next time, I feel that it will be risky and that we should go in expecting at least one more rollback. Today’s lesson was that it’s a bit tricky.
What would be pleasant would be if I were to think of a small simple set of steps toward getting the Grouper done in a way that makes sense to the entities in the world, with or without an explicit WorldEntity class. So far, the steps seem a bit large. We’ll let the little grey cells think while we do other things.
A bit more thinking, a bit more doing … we’ll get there soon. And then: Grouper!
See you next time!