We have some content that is fixed throughout game play, and some that varies. We’re getting close to needing to handle that. Perilously close.

Yesterday we wired in a simple “treasure”. I want to talk about that a bit, because it is a characteristic way that I work. In a word, well, two, that way is Very Incrementally.

In my many decades of programming, I have studied many of the masterworks on software design, and have tried a major fraction of what they advise. For most of my career, the common element seemed to be to design first, then implement the design. Whenever that approach went wrong, and it frequently did, the same refrain always appeared: You didn’t design enough. You didn’t really do what we said.1

I believe it was Kent Beck who first said, in my hearing, that we should let the code participate in the design. I may have misinterpreted that advice, as I have done with so much advice, but what thinking about that has led me to s

Something Like This
We need a new feature, say treasure in the dungeon. Treasure will be positioned in some cell, and it will probably be visible there, and when you find it, it will go to your inventory and disappear from the dungeon cell.

I think of the simplest thing I can (another bit of Beckian Advice). I think something like “ok a dictionary from xy to treasure”. I implement that as simply as I can imagine, which comes down to a dictionary addressed by xy, pointing to a string. I build that, perhaps with a test.

It needs to display, so I add some truly simple code to go through the treasure dictionary and draw a green dot in each cell in the dictionary.

What we have at that moment is a stupidly simplistic unsatisfactory solution … that works. We can place a treasure and see where it is.

What is most significant about this, I suggest, is that no sensible design for this feature would be anything like that. Someday, here, we’ll get to a sensible design, and I’ll try to remind us of this, but it’s clear that only a fool would imagine that a dictionary pointing to a string was a good idea.

I am that fool.

Then, in the next session, I replace the dictionary contents with a list, because there could be more than one treasure in a place. And to distinguish two treasures in one place, I build a tiny object with a draw method that draws a polygon, and I make two of them, with two different polygons, and I put them in the same cell and by golly we see both polygons.

In the design up front scheme, what always went wrong was that we’d be a ways in and would encounter some issue with the design that made the code hard to write, or that was simply missing, and we’d hammer and bash and bend until it worked.

In the Very Incremental scheme, the design is always wrong, and that’s much better, because it is also as simple a thing as we could think of that did just what we have so far, and not much more. We’re in a perfect position to discover more of what needs to be in the design, and we design it and put it in.

I’m not here to fight the Big Design Up Front war over again. I’m just reporting what I do, what happens, and how it’s different from what might happen if I went another way.

One way it’s different is that sometimes I run into something that seems to require a big change, to a scheme different not just in detail from what we have, but structurally. And I have to change it, a lot. If only I had thought of that sooner … I wouldn’t have wasted those twenty lines of code. Of course, what I learned writing those lines is not lost.

We have that situation this morning.

The contents of the dungeon are in the Dungeon instance variable contents, a dictionary from xy to content items. There is presently a small placeholder class, ContentItem, that I expect will grow into treasures and traps and whatever variable items might appear as we grow this program.

The issue is this: we need to be able to remove a content item after it is consumed, in whatever way it might be consumed. And the item does not know where it is, and it does not know how to get itself removed. It has no object to talk to that can handle that.

So We’re in big trouble, guys!2 Our design has a gaping hole in it.

To really deal with this issue, ISTM we have to begin to deal with how Intrepid Adventurer Dot will interact with things in the dungeon, such as treasures. The only human factors that we have now is that Dot can move around with the arrow keys. So, at least for now, if she steps on a cell containing a content item, we want to give her that item and remove the item from the cell.

Generalizing a little bit, since we know that not all content items are treasure, let’s say that we’ll send each item a message indicating that it should interact with the player. The object can decide what to do about it.

I’m imagining a sequence like this:

  1. Dot steps on a cell containing two items.
  2. In turn, each item is sent interact_with_player, including the player instance as a parameter. (Do we have a Player object? I doubt it.)
  3. Treasures give themselves to the player instance (whatever it is)
  4. Treasures remove themselves from the Dungeon.

In fact there is no Player object:

class Dungeon:
    def place_player_at_offset(self, cell, offset):
        new_cell = self.layout.at_offset(cell.xy, offset)
        if new_cell is None or self.layout.is_available(new_cell):
            new_cell = cell
        self.place_player_at(new_cell)

    def place_player_at(self, cell):
        self.player_cell = cell

The Dungeon just knows where to draw the Dot. Maybe we’ll just pass the Dungeon to the interact method and let it call us back. It’s not good design but it is probably a step in a decent direction. The good news is that the Dungeon is in a position to remove the item from the dungeon.

Reflection

I think we have enough to write a scenario in the form of a test. I don’t really like story tests but there is a story here and I think we need to tell it.

    def test_contents_to_player(self):
        layout = DungeonLayout(10, 10)
        room_cell = layout.at(5, 5)
        room = Room([room_cell])
        layout.add_room(room)
        dungeon = Dungeon(layout)
        item_1 = ContentItem("treasure")
        shape = [(0.33, 0.66), (0.66, 0.66), (0.5, 0.33)]
        item_2 = ContentItem("more treasure")
        dungeon.place_content_at(room_cell,item_1)
        dungeon.place_content_at(room_cell,item_2)
        dungeon.place_player_at(room_cell)
        assert item_1 in dungeon.inventory()
        assert item_2 in dungeon.inventory()
        assert dungeon.contents_at(room_cell) == []

There is no inventory, so we’ll start there.

class Dungeon:
    def __init__(self, layout):
        self.layout = layout
        self.player_cell = None
        self.player_inventory = []
        self.contents= defaultdict(list)

Now we do need an interact function on ContentItem. Let’s TDD that.

class TestContentItem:
    def __init__(self):
        self.inventory = []

    def receive_item(self, item):
        self.inventory.append(item)

    def test_interact_gives(self):
        item = ContentItem("treasure")
        self.inventory = []
        item.interact_with_player(self)
        assert item in self.inventory



class ContentItem:
    def interact_with_player(self, player):
        player.receive_item(self)

We have tested that the item will send receive_item to its player parameter. But Dungeon does not yet ask for the interaction. We need to change this:

class Dungeon:
    def place_player_at(self, cell):
        self.player_cell = cell

Something like this:

    def place_player_at(self, cell):
        self.player_cell = cell
        for item in self.contents[cell.xy]:
            item.interact_with_player(self)

The test now fails because Dungeon does not understand receive_item, just as planned. So:

    def receive_item(self, item):
        self.player_inventory.append(item)

Now the test fails for want of inventory.

    def inventory(self):
        return self.player_inventory

Now, I hope, it is failing because the cell is not empty. Yes.

Now we have a small issue. How do we know what to remove and where?

We have the player cell so we can do this:

    def receive_item(self, item):
        self.player_inventory.append(item)
        content = self.contents_at(self.player_cell)
        if item in content:
            content.remove(item)

I was surprised that that didn’t work. Then I realized that we’re iterating on the contents when we give, and removing from it when we receive, and you can’t go around removing things from lists that you’re iterating.

    def place_player_at(self, cell):
        self.player_cell = cell
        for item in list(self.contents_at(cell)):
            item.interact_with_player(self)

Asking for list() copies the contents list, and we can safely iterate the copy. We have a test that will fail if we were to remove that.

It seems likely to me that if I run Dot over the treasures on the screen, they’ll vanish from mortal ken.3

And that is exactly what happens.

before: two triangles represent contents

after: two triangles are gone from dungeon

The two triangular treasures are present before Dot steps on them, and gone, into inventory, after she has passed by. This is what we expected.

Commit: content items run over by player interact with player and can give themselves to her.

Let’s sum up.

Summary

We recognized an issue, the removal of items from the dungeon upon giving them to the player. We wrote a couple of tests, one that checks that an item gives itself to the parameter provided in interact_with_player, and one that checks that the item goes to the player inventory and is removed from the dungeon.

We kind of fell into finessing the question of a Player object, realizing that we can pass the dungeon down. If at some future time we have a Player object—and I suspect that we will—the dungeon will certainly know the player and can forward messages as needed.

I have learned, both by rote and by experience, that objects should not know their owners, so when we do build a Player, I’ll be tempted to give her a pointer to the dungeon, but will try to resist, instead passing an object to call back to when it is needed.

The key good thing about these small steps is that they have given us the connection between dungeon contents and player inventory. It is a very thin connection, but it seems clear that it can be made more robust as needed.

So a tiny decision, just a few small methods, and we have the rudiments of a key feature.

I love it when the lack of a plan comes together. See you next time!



  1. I do not fail to note the irony here, because when “Agile” approaches failed, and they frequently did, our first reaction was much the same: You were doing it wrong. Plus ça change … 

  2. An homage to Ed Anderi, who spotted more issues in our design than any other individual. 

  3. Ken? There is no Ken in this game. I’m not confused, you’re confused.