The Repo on GitHub

Woke up at 3 AM with an idea. Going to work on it. I have opened my zoom in case anyone else on the team wakes up. Pairing, as always, is tricky but rewarding.

Hey, it’s better than thinking about sad things at 0300 hours. I can do something about these ideas.

Our present Bot moves, picks up blocks and drops them a while later. The “real” spec is that it picks up blocks (probably blocks that are “alone”), and drops them adjacent to other existing blocks. The idea is that these simple behaviors should engender a kind of overall organization, with blocks tending to wind up in groups.

So we need a bit more intelligence about when to pick up, and when to drop off. I propose, roughly, the following:

Whenever the Bot moves, it will receive, automatically, a “vision” result, showing the content of the 9 cells surrounding it. (Scan is left open. We are sort of leaning to the idea that a scan shows a wider area, in less detail, and may cost you a turn.)

The current World map is … what, exactly? An Entities collection:

class World:
    def __init__(self, max_x, max_y):
        self.width = max_x
        self.height = max_y
        self.map = Entities()

An Entities collection is a map from entity ID, the unique integer assigned to each object by World, to the Entity. An Entity is currently either a roBot or a Block, and they each have a name (‘R’ for Bot and ‘B’ for Block) and location, a Point object, which contains x and y integer coordinates.

Note
At this point, there is no server-client distinction in the program. We do know that it will have to become a world server with many entities coming from different clients, and we are intentionally ignoring that inconvenient fact, partly because it makes for faster progress here in early days, and partly to see how much trouble we get into by not making server-client design decisions up front.

We do, however, try to remain somewhat aware of what the two sides can know about each other. The Bot should make no accesses to the World, other than a few accepted messages, like move, take and scan, and the World only accesses the Entity id (which it has assigned) and its name and location. The world does send at least one message to the Bot, receive, when the bot has issued a take and there was something to take.

I propose that, upon move, the World will send another message to the Bot, vision, which will consist of a list of the items in the Bot’s immediate vicinity. I think we need a new object here, something with a name, x, and y, a simple data record, to pass back and forth.

I think I’d like to spike this and see what happens. I am up to date.

Let’s see about a test for this new thing. I’ll make a test_vision file.

    def test_nothing_near(self):
        world = World(10, 10)
        bot = Bot(5, 5)
        world.add(bot)
        bot.vision = None
        bot.move()
        assert bot.location == Point(6, 5)
        vision = bot.vision
        assert vision is not None

Create a world and bot, put bot in world, move bot, make sure it moved. From vision starting at None, ensure we got something in vision. Test fails on the final assert. I added a member init to Bot, because we like to have all our members declared in the init.

class Bot:
    def __init__(self, x, y, direction=Direction.EAST):
        self.world = None
        self.id = None
        self.name = 'R'
        self.location = Point(x, y)
        self.direction = direction
        self.direction_change_chance = 0.2
        self.inventory = []
        self.vision = None
        self.tired = 10
        self.state = "walking"

I note that Bot class is getting pretty busy. That suggests that it may need some kind of refactoring.

We are here to put more stuff in the swamp, not to drain it. Let’s make vision a property, referring to a private _vision for now.

PyCharm provided the code below after I typed @property and def vision:

    @property
    def vision(self):
        return self._vision

    @vision.setter
    def vision(self, vision):
        self._vision = vision

Pretty sure that’s exactly what I had in mind. Now, in World:

    def move(self, entity, direction):
        self._move(entity, direction.x, direction.y)
        entity.vision = self.create_vision(entity.location)

    def create_vision(self, location):
        return []

I really expected my test to pass now. It does not. Why not?

Ah. Bot does not use move on World, it uses step, which calls _move. We probably should sort that out, see what move is even good for. For our test:

class World:
    def _move(self, entity, dx, dy):
        entity = self.map.contents[entity.id]
        location = entity.location
        new_x = self.clip(location.x + dx, self.width)
        new_y = self.clip(location.y + dy, self.height)
        entity.location = Point(new_x, new_y)
        entity.vision = self.create_vision(entity.location)

OK, we are passing something to the Bot, now let’s pass something meaningful. I’ll enhance the existing test to be correct as to what vision is in this case:

    def test_nothing_near(self):
        world = World(10, 10)
        bot = Bot(5, 5)
        world.add(bot)
        bot.vision = None
        bot.move()
        assert bot.location == Point(6, 5)
        vision = bot.vision
        assert ('R', 6, 5) in vision

The vision should be a list of triples (e.name, e.x, e.y), for the nearby entities, which will, because I say so, include the Bot, whose name is ‘R’.

We should probably make a little object of that. For now, using Fake It Till We Make It:

    def create_vision(self, location):
        return [('R', 6, 5)]

Green. We could commit, but I’m spiking so I won’t.

New test, more difficult:

    def test_three_blocks_near(self):
        from block import Block
        world = World(10, 10)
        world.add(Block(4, 4))
        world.add(Block(6, 6))
        world.add(Block(4, 5))
        bot = Bot(6, 5)
        bot.direction = Direction.WEST
        bot.direction_change_chance = 0.0
        world.add(bot)
        bot.vision = None
        bot.move()
        assert bot.location == Point(5, 5)
        vision = bot.vision
        assert ('R', 5, 5) in vision
        assert ('B', 4, 5) in vision
        assert ('B', 6, 6) in vision
        assert ('B', 4, 5) in vision
class World:
    def create_vision(self, location):
        result = []
        for dx in (-1, 0, 1):
            for dy in (-1, 0, 1):
                found = self.map.entity_at(location.x + dx, location.y + dy)
                if found:
                    result.append((found.name, found.x, found.y))
        return result

We just search for entities in the 3x3 square around the bot’s location. Test passes.

Time to think

Reflection

The main concern I have with this is that map.entity_at is rather slow:

    def entity_at(self, x, y):
        point = Point(x, y)
        for entity in self.contents.values():
            if entity.location == point:
                return entity
        return None

That entails a full search of the entities table. (It is also assuming one entity per location, which is not enforced. We should make notes about things like that.)

I have made a notes.md file in the repo.

And now …

Now I can start working on the cool thing I wanted to work on when I was trying not to wake up.

I just got one thing slightly started:

    def test_a_pattern(self):
        vision ='B_B'\
                '_RB'\
                'B__'
        pattern = r'B_..R....'
        result = re.search(pattern, vision)
        assert result
        pattern = r'.B._RBBBB'
        result = re.search(pattern, vision)
        assert not result

Mostly what that is showing me so far is that the pictures are not as easy to make readable as I thought, and that recognizing them may not be as compact as I’d like. But it’s early days.

And then …

Bryan showed up, and shortly thereafter GeePaw returned. We worked on picking up and dropping a bit.

We decided that you can only pick up and drop off using the square in whatever direction you are facing. (The Bot’s direction member specifies which way you will move and, therefore, I guess, which way you are facing. It can be changed without cost on the Bot side and is used on your next move, take, or drop.)

The most important changes include this:

    def drop(self, bot, entity):
        location = bot.location + bot.direction
        if not self.map.entity_at(location.x, location.y):
            entity.location = location
            self.add(entity)
            bot.remove(entity)

We check the location “in front” of you, that is, one step in whatever direction you are going. If there is nothing there, you can drop, and we place the entity you gave us to drop and send you a message to remove it. So if you can’t drop it, you don’t get the message.

The Bot’s logic changed accordingly:

    def remove(self, entity):
        try:
            self.inventory.remove(entity)
        except ValueError:
            pass

    def do_something(self):
        if self.state == "walking":
            if self.tired <= 0:
                self.state = "looking"
        elif self.state == "looking":
            if self.beside_block():
                self.take()
                if self.inventory:
                    self.tired = 5
                    self.state = "laden"
        elif self.state == "laden":
            if self.tired <= 0:
                block = self.inventory[0]
                self.world.drop(self, block)
                if block not in self.inventory:
                    self.tired = 5
                    self.state = "walking"
        self.move()

In ‘laden’ state, we check after the drop to see if the block is gone, and only if it is, do we set the tired counter and change back to walking.

This is not entirely robust yet, I believe. Certainly we’re assuming here that we want to drop the first thing in inventory and although the inventory is a list, we have no support for there being more than one thing in it, and no protection against it happening.

We still have not gotten around to fixing up the take: we just go around trying take everywhere and sometimes it gives us something. When it does, we enter the ‘laden’ state.

It’s early days, and we are progressing.

Today’s pairing was sometimes awkward. I felt that I was getting advice that didn’t fit what we were trying to do, and I think Bryan felt we were trying to do something different from what I thought we were trying to do. GeePaw helped by pointing out that there were at least two things we might be doing and that perhaps we should pick one.

I continue to try to figure out what causes us difficulty in pairing. I am quite sure that Bryan and I parse problems and solutions differently, though I can’t put a finger on the difference. I don’t think one way is better than the other, it’s just different, so we have trouble following the other. I am actively trying not to push my ideas when he is driving, but instead to figure out what I can of what he’s up to and supporting it with small suggestions, mostly syntax issues or an occasional hint of a line to try.

We had one interesting moment that was a bit frustrating. We were scanning the code for things to work on and I mentioned that GeePaw and I had agreed that this __init__ suggests that the object is too big:

class Bot:
    def __init__(self, x, y, direction=Direction.EAST):
        self.world = None
        self.id = None
        self.name = 'R'
        self.location = Point(x, y)
        self.direction = direction
        self.direction_change_chance = 0.2
        self.inventory = []
        self._vision = None
        self.tired = 10
        self.state = "walking"

I was surprised when Bryan began obviously thinking about the method and kind of trying to understand it, sort of classifying the different bits into conceptual chunks. Nothing wrong with that, and probably useful if and when we decide to do something about it. But I was just flagging a code smell with no thought of moving beyond “wow that’s big, probably this object needs refactoring”.

Again, there’s no right way. And everyone’s way is a bit different. I have reason to suspect that my way is quite often rather more intuitive and rapid-response than most folks’ ways. Is it better? Heck, I don’t know. I manage to find my way around, almost never get lost on the way to the living room. It’s just how my mind works, for better or worse.

But it does make for a bumpy ride sometimes.

Relatedly, I tend to notice things, possibly note them, and put them aside. I couldn’t possibly work on all the things I notice all at one time, so I will perhaps write them on a card or sticky note, and may or may ot review the notes later. A large percentage of things that I notice get taken care of in the course of things, and some do not. Some I don’t even write down, though I do try to note the things that seem to me to be able to lead to trouble.

For example: the Entities collection can have multiple objects at the same coordinates, because it just stores things by id and the coordinates are just data. We have changed drop just now so that it cannot drop things on top of things … but we have not changed anything to prevent the creation of two objects with the same coordinates during World.add. I wanted to make a note of that, and consider it to be a question: should we allow more than one thing at the same coordinates, or should we not? The code is not correct for either case at the moment. We were frustratingly unable to communicate clearly on that topic at the time, although I am sure that now, we are closer to understanding. I think.

People are different. This is why pairing all day is hard work: we are continually rubbing against a rough interface, and if the pair ever get too much on the same page, their work is probably less effective. There needs to be just enough difference to broaden the scope of the problem and solution space, but not so much that we cannot understand each other. It is a continuing balancing act, different for every pair.

As usual, programming is more about people than it is about code. Much as we might wish otherwise.

It’s going well enough to please me greatly, and I hope that GeePaw and Bryan feel about the same.

See you next time!