It’s Pi Day, Sunday, and I think we’ll take a look at our new Strategy object with fresh eyes, if we have any.

When the Deity looked upon Her work, it is said that She found it good. I personally feel that improved factoring and functionality would have made a better product, but there’s no explaining Deities.

As for my own code, I look upon it and almost always see something that could be better. And that is as it should be. My friend GeePaw Hill points out that software developers are in the business of changing code. Sure, we write some new code, but it’s rare that we ever write it in an open space, unconnected to, unwoven into, a mass of existing code. Our purpose, almost always, is to change what that code does. And, if we’re good at our job, we change how the code is, how it is shaped, how the new lives with the old.

Part of our job is to recognize aspects of the code. Where do we need to change it? Are the things it does well arranged? How can we make it better while we change it?

That’s where I want to start this morning.

Yesterday

Yesterday, we created a new CalmMonsterStrategy class, embodying the essence of our new more mellow monster behavior. That class is quite simple. Here it is, in its entirety:

CalmMonsterStrategy = class()

function CalmMonsterStrategy:init(monster)
    self.monster = monster
end

function CalmMonsterStrategy:execute(dungeon)
    local method = self:selectMove(self.monster:manhattanDistanceFromPlayer())
    self.monster[method](self.monster, dungeon)
end

function CalmMonsterStrategy:selectMove(range)
    if range > 10 then
        return "basicMoveRandomly"
    elseif range >= 4 then
        return "basicMoveTowardPlayer"
    elseif range == 3 then
        return "basicMaintainRangeToPlayer"
    elseif range == 2 then
        return "basicMoveAwayFromPlayer"
    elseif range == 1 then
        return "basicMoveTowardPlayer"
    else
        return "basicMoveAwayFromPlayer"
    end
end

Looking at that code in the cool light of Pi Day, what do I see?

First of all, it’s very short. That isn’t bad. It’s probably good. Second, it returns the name of a method, which is intended to be executed in the line:

    self.monster[method](self.monster, dungeon)

It is entirely possible that Monster will not understand that method. A simple typo would suffice to cause that, and if it did happen, the game would crash. So we see that this code is not robust.

Mind you, it wasn’t robust the day before yesterday, either. We’ve been just returning string methods for a long time, but we might want to think about the inevitable mistake that we’ll surely make sooner or later. Tpyogarphic errors are quite common.

What if we were to have an alternate strategy for some monsters at some time. We used to have a more aggressive monster behavior, where, once they came within ten tiles, they just kept coming, getting inexorably closer and closer, moving toward you in a sinister fashion, coming, coming until SUDDENLY THEY ATTACK!

What if we wanted to put that in, such that either some monsters are always aggressive, or such that some become aggressive under circumstances yet to be defined? What would we have to change?

If there was to be another strategy such as NastyMonsterStrategy, it seems that it would have the same form as this one, but a new selectMove method. That makes me wonder whether this scheme is quite right.

We plug in the strategy here, in Monster:

function Monster:initAttributes()
    local health = CharacterAttribute(self:roll(self.mtEntry.health))
    local strength = CharacterAttribute(self:roll(self.mtEntry.strength))
    local speed = CharacterAttribute(self:roll(self.mtEntry.speed))
    local attrs = { _health=health, _strength=strength, _speed=speed }
    self._characterSheet = CharacterSheet(attrs)
    self._movementStrategy = CalmMonsterStrategy(self)
end

So a monster that was always Nasty would have its movement strategy initialized right there. A monster that became enraged and Nasty might pop a new strategy into its movement behavior. Easy enough, and the overhead of a whole class isn’t enough to bother me. The instances are small. I observe this concern, and don’t see the need to change it at this time. We’ll keep an eye on the situation.

There’s another related question to consider. I’ve read that in “real” games, the NPCs1 are operated by an “AI”, a doubtless misnamed bit of code that gives them their complex behavior.

We don’t have a place for an AI right now. We don’t have any decision process for selecting our move strategy; we only have the one, and we broke it out because–well–because I wanted to. But we will have a decision process for monsters. I have a few things in mind:

  • Venomous monsters should only attack until they manage to poison the player. At that point, they should flee, or if they are bees, die.
  • There might be monsters that lead the player to interesting places, for values of “interesting”.
  • There might be “companion” monsters, who ally themselves with and assist the princess.
  • There could even be more intelligent NPCs. Wayne Rasmussen suggested quests like “Find a lost book and return it to the Librarian”. That could be fun.

Capabilities like this would need to be embedded somewhere, but where? Right now, they might be embodied as new movement strategies, or they might be embodied somewhere inside the Monster code. There might even be other kinds of Entities. One way or another, we will need to find sensible places for code like that to live. And, frankly, I’m not entirely happy with the choices.

The Monster class is 279 lines. Player is 229. GameRunner is 370. CalmMonsterStrategy is 29.

I feel sure that Monster, Player, and GameRunner are too large. I know that some Monster code is about creating the monsters and could be moved to a MonsterFactory. (Maybe it should be the MonsterCreationLaboratory.) GameRunner is similar, with a lot of dungeon creation code that could be moved.

Now here’s a thing.

When we build a system from scratch, incrementally, it is quite natural to add new capabilities to the code we already have. It seems like a bigger deal to create new classes, and, since our ideas are just taking shape, we often don’t even know what class we’d build. There is a natural tendency for classes to grow beyond ideal.

What is “ideal”, you ask? A class should have essentially one responsibility. The harder it becomes to describe it without conjunctions, the more likely it is that it has more than one responsibility,

What does GameRunner do? Well, it sets up and runs the game. There it is. You can’t fairly describe it without the “and”. What else does it do? Well, it draws the large and small maps. Oh, yes? What else? Well, it serves as a central point of communication among the system’s objects. Oh, does it? What else does it do? Well, it knows and emits all the messages at the start of a level. Ah, I see. What else? Well, it creates the Buttons for the player to press.

Oh, is that all? Maybe.

Something about that list should tell us that the GameRunner is a big ball of stuff that was glommed together. Now we’ve moved some of that stuff out. For example, the Monsters class has taken over some of the responsibility for monsters, including creation. It’s only about 30 lines of code, by the way, with another 30 or so for testing it.

So we look upon our work, and we see that it is good–and that it could use some improvement.

So long as we can see both those things, we’re probably doing OK.

Shall we code a little something? Let’s do.

What Shall We Do Today, Brain?

Well, it’s Sunday (as well as Pi Day). Let’s do something simple. Here’s a story:

  • There should be “aggressive” monsters, ones that keep coming at you.

We ask: How should this be decided? And we answer: I don’t know yet. Do something simple for now just to get it working.

We say: we can do that.

Our current design says that there should be a NastyMonsterStrategy. And I imagine that we should TDD it, but we know what it’ll look like, so we can paste that in:

NastyMonsterStrategy = class()

function NastyMonsterStrategy:init(monster)
    self.monster = monster
end

function NastyMonsterStrategy:execute(dungeon)
    local method = self:selectMove(self.monster:manhattanDistanceFromPlayer())
    self.monster[method](self.monster, dungeon)
end

function NastyMonsterStrategy:selectMove(range)
end

I can’t bring myself to test this. Here’s what it has to be:

function NastyMonsterStrategy:selectMove(range)
    if range > 10 then
        return "basicMoveRandomly"
    else
        return "basicMoveTowardPlayer"
    end
end

How to test this? I’ll make them all Nasty and play.

ghost

Well, there’s no question that that works. What shall we really do? Let’s make one monster in, oh, five, nasty.

function Monster:initAttributes()
    local health = CharacterAttribute(self:roll(self.mtEntry.health))
    local strength = CharacterAttribute(self:roll(self.mtEntry.strength))
    local speed = CharacterAttribute(self:roll(self.mtEntry.speed))
    local attrs = { _health=health, _strength=strength, _speed=speed }
    self._characterSheet = CharacterSheet(attrs)
    self._movementStrategy = self:selectMovementStrategy()
end

function Monster:selectMovementStrategy()
    if math.random(1,5) == 5 then
        return NastyMonsterStrategy(self)
    else
        return CalmMonsterStrategy(self)
    end
end

This totally has to work. Commit: Twenty percent nasty monsters.

It might be nice to have an indication that a given monster is nasty. What if we could make some indication in the character sheet? No, it’s better to let it be a surprise. All the monsters will approach to within three tiles. And nasty ones just keep coming.

We’ll let this ride, and sum up.

Summary

We’ve reviewed the code and observed some points of concern. We’ve not undertaken to change anything, and that’s my preferred approach when working on a real product: improve code only when working on it anyway, and to the extent that it’s in our way.

I prefer this because my personal experience with product development is that if the sponsors don’t see a continual flow of capability that they have asked for, they tend to lose patience with the effort. That leads to pressure, and sometimes to abrupt changes in one’s budget or employment status. So I would always work toward a continuous flow of features.

Now, yes, we could work some percent of the time on features, and some on improving the code, but even then, improving code that’s not in the way isn’t very helpful. It may be helpful in the future, but it’s not helpful now. Improving the code where we’re working helps us now.

So that’s what I’d do. You get to do as you see fit.

But I do like to review the code, and perhaps even to make tiny improvements. One needs to refresh one’s mind, and it’s good to refresh memory as well. And often, after reviewing something, we’ll get an idea a day or so later for something to make things better. We make a note of that and wait for an opportunity to do it.

Now, of course, in these articles, I act differently. I might choose to refactor something when I don’t have a feature running through it. I do that because this isn’t a product development, it’s a demonstration of how I work. Or it’s a weird hobby where I develop things and write about them. It’s certainly not a product. No one is buying my Dung program.

Today, we’ve actually installed a new monster strategy–well, reinstalled an old one–and it plugged in very nicely, We have a rudimentary decision process, one in five monsters is nasty.

By the way, I played a long session in the game, just for fun, and at one point I was surrounded by three Ankle Biters. A fourth one showed up, and attacked me. The others just stood around. It seemed clear to me that the others should probably join in the battle, defending their brother or sister who was fighting me. Maybe they were just hanging back, thinking “What’s up with old Paul Ankle? Why did he do that? She’s gonna kill him!”

Anyway it seems there’s room for behavior changes. I also decided, after leading three of them around, that they should get bored and wander off.

I can feel a Monster AI coming, can you?

Lots to do. Lots to learn, even if dungeons aren’t your thing. All the programming problems look the same from where I stand.

See you next time!


D2.zip

  1. Non-Player Character. Our monsters are NPCs. Not very smart ones … yet.