The Repo on GitHub

The famous GeePaw Hill plans to join me this morning, at some point. Until that joyous time, I’ll think about things we might do. We do two good things and are faced with an unknown. Note: one more thing added.

I want to put a markdown file in the repo, listing, I don’t know, things. Stories, ideas, questions, notes, whatever. I think what I might do is write some notes here and then move them over, because I’m used to this sort of way of writing while thinking.

Ideas include, from cards and my head:

  • [!] Improve Bot take. Presently it just goes around trying to take all the time.
  • [!] Improve Bot take so that it tends not to take blocks that are adjacent to other blocks.
  • [!] Continue Bot implementation using the current not-smart-at-all approach. (Better define what we mean by that.)
  • Manage inventory more sensibly or make it much simpler.
  • Move toward client/server?
  • Handle errors
  • Discuss Story Tests and what Hill calls the Pyramid Problem
  • This card reads “Test groundtash, why arithmetic? domain not math, Obstruction.” What did I have in mind? [Ah! That says “test granularity”, per Hill’s observations.]
  • Use private methods in world. (I remember this one! The idea is to begin to make more clear which methods are there for the client and which are internal.)
  • [!] Don’t step on things.
  • Things on top of things: entities access by x,y
  • Clocking
  • Vision
  • Logging on screen.
  • [!] Indications on screen that bot is carrying etc
  • [!] Drive screen from world, not its own list
  • Drop off screen! (fixed?)
  • Robot eats robot. (fixed?)
  • Would Extract Method Object be a good move for do_something?
  • Should all Entities be sent do_something?
  • Keep track of inventory on server side?
  • Learn pytest fixtures?

I’ve marked a few of those with [!], indicating that I want to give them priority. There are too many. Most of them require consideration, ideally with other members of the team, if there are any available.

Now I’ll browse some code. I am optimistic that someone may arrive soon to pair with me.

I start PyCharm, run the tests, green, pull, up to date, run the tests, green, make a non change, see the tests auto-run. pin the test tab. Ready to roll.

I’ve been thinking a lot about how to make our bot tend not to pick up blocks that are touching other ones. Our current code in do_something is this:

class Bot:
    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:
                if self.near_block():
                    block = self.inventory[0]
                    self.world.drop_forward(self, block)
                    if block not in self.inventory:
                        self.tired = 5
                        self.state = "walking"
        self.move()

In the “looking” state, we ask whether we are beside a block and if so we try to take it. If we then have something in inventory, we enter the “laden” state. (We are not handling inventory sensibly. I’ll add that to the notes above right now. You won’t realize until you read this far that it was added after a while.)

I believe that beside_block returns True.

    def beside_block(self):
        return True

So we just wander around, snapping our pincers, hoping to pick something up. If we ever do, yay us, we enter “laden” and wander until we put the block down.

The simplest reasonable behavior (ant behavior as we model it) would be to notice when we are beside a block, like the method says, and if we are, just take it. We could use our Vision object to support accepting a block like this:

_B_
_R_
___

And reject a block when this is the picture:

BB_
_R_
___

I think we can even rationalize this as sensible ant behavior, maybe the doubled block just looks too big or something. What is more difficult, given that we can only see the 9 squares around us, would be to avoid picking up this block (I’ll show an additional row.)

__B
_B_
_R_
___

The Bot cannot see the block at the top right, because it can only see one cell in each direction. I was thinking that it might be fun to come up with simple rules that would avoid picking up the block in that situation. In the cold clear light of day, I think that’s too sophisticated for what we’re trying to do, at least right now.

So let’s see about giving the beside_block just a bit more wisdom.

Here’s the near_block method we use to decide whether and where to drop:

class Bot:
    def near_block(self):
        vision = Vision(self.vision)
        p1 = 'B_???????'
        if vision.matches(p1, self.location):
            self.direction = Direction.NORTH
            return True
        if vision.matches('__B??????', self.location):
            self.direction = Direction.NORTH
            return True
        if vision.matches('BBB??????', self.location):
            self.direction = Direction.WEST
            return True
        return False

I’ll leave that for you to decode. I think I would like to have some tests for beside_block: it seems only fair.

I’ll put them in the test_bot file. Seems somewhat apropos.

And just in the nick of time, Hill arrives.

We worked a bit on making the Bot pick up only blocks in front of itself. We were fiddling with the pattern idea, and some other stuff and suddenly we lost the thread. If I had been alone, there would be 100 or so lines of me losing the thread. Because I can’t write contemporaneously and pair at the same time, I only have a three track mind, you are spared that. We rolled back and thought a while.

We decided that we had taken too big a bite, though we were not quite sure what was too big, and we decided to do a simpler “story”, namely that the Bot will always take a block that is right in front of him, if he is in the looking state.

Then we wrote this test:

    def test_bot_facing_north_block(self):
        map = Map(10, 10)
        bot = Bot(5, 5)
        bot.direction = Direction.NORTH
        map.place(bot)
        bot.vision = map.create_vision(bot.location)
        assert not bot.facing_block()
        map.place(Block(5,4))
        bot.vision = map.create_vision(bot.location)
        assert bot.facing_block()

He’s facing north. There is no block in front of him, so facing_block should be False. Then there is a block and it should be True. We coded that right up, without any of that cute pattern stuff:

class Bot:
    def facing_block(self):
        look_at = self.location + self.direction
        name = self.vision.name_at(look_at.x, look_at.y)
        return name == 'B'

Well, I tell a lie. We went in two steps, because when we came in, the vision property of the bot was that naked list of (name, x, y), and we wrapped it in a Vision object right in the methods. Then we changed the vision property:

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

That gives us access to the pattern-checking method matches, and also to name_at, renamed from vision_at:

class Vision:
    def name_at(self, x, y):
        for name, vx, vy in self.vision_list:
            if vx == x and vy == y:
                return name
        return '_'

Now the bot can only take a block if it is right in front of him … and he will do so in that case even if other blocks are near it. If we want him to ignore blocks that are connected to others, we’ll have to improve his logic.

We wrote one more test to verify that the facing thing was working:

    def test_bot_facing_east_block(self):
        map = Map(10, 10)
        bot = Bot(5, 5)
        bot.direction = Direction.EAST
        map.place(bot)
        bot.vision = map.create_vision(bot.location)
        assert not bot.facing_block()
        map.place(Block(6,5))
        bot.vision = map.create_vision(bot.location)
        assert bot.facing_block()

It passed. We spoke of writing the other two, but we agreed that our implementation, plus these two tests, clearly works. YMMV and if my pair wanted to write the other two tests, I’d certainly go along with it.

Then, more or less randomly, we looked at the state-handling code in do_something:

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

Hill refers to that a “slalom code” and I suspect you can see why. We spoke of how to fix it. I allowed as how I’d been thinkin of Extract Method Object. Hill mentioned converting the state variable to a lambda or method name or whatever Python did, and we did that. First we extracted three methods from do_something:

    def do_something(self):
        self.state()
        self.move()

    def laden(self):
        if self.tired <= 0:
            if self.near_block():
                block = self.inventory[0]
                self.world.drop_forward(self, block)
                if block not in self.inventory:
                    self.tired = 5
                    self.state = self.walking

    def looking(self):
        if self.facing_block():
            self.take()
            if self.inventory:
                self.tired = 5
                self.state = self.laden

    def walking(self):
        if self.tired <= 0:
            self.state = self.looking

As originally extracted (we failed to do a commit at that point), the methods were still setting state as a string, and do_something looked like this:

        if self.state == "walking":
            self.walking()
        elif self.state == "looking":
            self.looking()
        elif self.state == "laden":
            self.laden()
        self.move()

Then we initialized state as shown here:

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

And changed all the setting of state from e.g. state = 'walking' to state = self.walking. Note that we set state to the method, not to the result of calling the method. The method (name) in state is called in do_something:

    def do_something(self):
        self.state()
        self.move()

That just worked. We were pleased but not terribly surprised.

So the state logic is much more like a state machine, since it has actual states (which are blocks of code) and it executes its state, which may change the state.

We are closer to being able to extract the state logic into a separate object, should we feel the need.

The code was much better by our lights, and we committed.

One More Thing

I forgot to mention this. There was a method called create_vision in World:

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

That method has terminal feature envy, since it really only refers to the map. So we moved the method to Map, modified it to work, and inlined the call to it:

class World:
    def step(self, bot):
        location = self.bots_next_location(bot)
        self.map.attempt_move(bot.id, location)
        bot.vision = self.map.create_vision(bot.location)

class Map:
    def create_vision(self, location):
        result = []
        for dx in (-1, 0, 1):
            for dy in (-1, 0, 1):
                found = self.entity_at(location.x + dx, location.y + dy)
                if found:
                    result.append((found.name, found.x, found.y))
        return result

Makes much more sense there!

Summary Discussion

We summed up mostly by thinking about the future. We want to hold off on converting to a server-client structure for a while, for pedagogical reasons (we think we’ll learn something and so will our readers), but we are very concerned about the fact that we do not actually know how to set up a client server situation in Python at all.

So we’ll have to work on that.

I didn’t get around to adding my list above into the repo, but I’ll do that soon. For now, I deserve a break, and so do you! See you next time!

See you next time!