Dungeon 254: What if ...
I found myself wondering what I’d do with this program if I started over. I shouldn’t do that. But what I could do …
It’s Wednesday morning, which means that the Friday Evening Zoom Ensemble met last night. We were looking at B’s dungeon code, and I found myself making observations about B’s that applied to mine as well. I went on to say that I’m tempted to start over, to see what I’d do differently next time, but that doing so is rather against my principles.
Principles, you ask? I do have them, and if you don’t like them, I have others1. In this case the principles in question are those that tell me to refactor rather than rewrite. I believe, based on some experience, although not all experience, not even all of my own, that when our program’s design isn’t what we need, we can “always” refactor smoothly to get to where we need.
Now this is manifestly false. At least I think it is. I think I could exhibit a program that was so fouled up and so wrongly designed that replacing it would be the only hope of getting from that hideous soul-shriveling design to a decent one. But most of the time, most of us don’t write that program. We who live in the world of TDD and refactoring generally write pretty good programs that pretty much contain all the design ideas we had at the time, and our design ideas, while they may not be the Designs of the Gods, are probably reasonably sensible, fairly rational, and somewhat clear.
Those designs are generally malleable enough to be improved. Improving existing code is, I’m sorry to say, almost always less expensive than rewriting it, and unquestionably always delivers value sooner, since we can make a small improvement today and ship it, while with a rewrite we have to wait the six months we said it could be done in, and then the additional nine months that it actually takes2.
Therefore, the story I tell is that when our design isn’t cutting it, it’s probably best not to try to start over, unless we’re still in the first few weeks of the effort, but instead to improve the design in place, in situ, and starting from where it is rather than starting way over there with a clean sheet of paper, no matter how much we simply love the feel of clean sheets.
And therefore, what I’m going to do for a while is to review the design of D2, my infamous Dung[eon] program, and to figure out what I wish the design were, and to determine whether and how we might be able to get to a better place. And I’m going to start that real soon now. But first …
Technical Debt?
Am I dealing now with technical debt? Not really, not if you want to stick to the classical definition. Technical debt is the difference between the design we have, and the design we now know that we would like to have. That’s not today’s case. Why? Because, today, right here at 0936 hours, I don’t have much of an idea about a design that would be better. Perhaps by the end of the morning or the week, we’ll have a better design to wish for.
At this writing, I am willing to say that we’re not dealing with a bad design. Oh, we’ll find things to improve wherever we look, and I am sure that we’d benefit from some sharp lines being drawn between things that are entangled now, and so on. But I’ve been working in this program for over 250 days, and it’s not really hard to work with. I’ve tried to keep it clear and tidy3.
So what is happening here? Well, think of it as popping our heads up many levels from yesterday’s fiddling with the Finite State Machine class, to take a series of looks at the overall design features of this program, to assess what we like about them, and what we don’t like, and to see what we may be able to do about moving the program toward a better place4.
How Does This Thing Work?
Let’s talk about some of the main classes and ideas in this thing. There are 90 classes, 881 functions or methods, 51 tabs, 8914 lines, and 241419 characters in the D2 program. That’s a summary of the output of my nascent Making app and ProjectInfo app.
Hmm, that’s about 10 lines per function/method. I space between each, so that’s 9 lines per. Each one starts with a function
line and ends with end
, so that suggests an average method length of seven lines. Perhaps not too bad. But I know there are some huge ones in there, and those will not be anything to be proud of.
As a side project, I should return to working on Making and ProjectInfo, combine them, make them actually useful. Here’s what they look like now:
That last picture gives us a design hint right there. GameRunner class has so many methods that they won’t even fit on a display page. There must be about 60 methods in there. That is surely an indication of something.
Now, as it happens, there is one lesson that I have tentatively “learned” already, which is that were I to do this all over again, I would make more use of the publish-subscribe model of communication, which would probably remove some responsibility from GameRunner, which serves as a communications hub between objects. But publish-subscribe can’t handle all the questions we may have in “real time”.
For example, a Monster needs to decide which way to move. It has some kind of trivial “AI” that it uses to decide whether to move randomly, or toward the Princess, or away from her. To do that it has to know the distance and direction to the Princess. Doing that via publish-subscribe might work, but I get a little concerned about all the entities in the dungeon getting messages about where the Princess is all the time.
But … on the other hand … they’re all likely to ask anyway, so maybe if they just “know” it would be better.
- Refactoring Digression
- Suppose we were to decide, yes, instead of everyone asking “Where’s the Princess?”, we’d like to just have her publish her position every time she moves, and anyone who’s interested can subscribe to the Princess News Report.
-
We could very likely make that change quickly and independent of anything else. It’s surely easy to set a publish call wherever her position changes. With any luck, that’s just one location, and if it isn’t, we can make it so. Similarly, since all the Monsters are basically the same class, we can simply add a new member about princess location, subscribe to the news, and voila! all the Monsters know where she is.
-
I believe that most design improvements are like that, and that’s why I believe we can “always” refactor to an adequately good design, given only that we’ve maintained a reasonable design all along.
Anyway, back to how it all works. Let me describe how I think it works, prior to diving into the code.
The game is played on a big 2D array of Tile instances. Each level is created as a rectangular array of Tiles, Each Tile has contents
, a collection containing anything that is “on” that tile, typically a treasure or decor item. Most Tiles have no contents. If I recall, Tiles know how to draw themselves. It’s possible that they contain some sub-object to do that: I’d have to look to be sure.
In the game, there are Entities, either the player (Princess) or Monsters. Entities have game-related properties, such as strength. Monsters have associated information such as the level of the dungeon they appear on. They all have a sequence of animation frames, which are small pictures (textures) that the game displays when the monster is on the screen. They typically have at least a couple of movement frames, and a dead frame. The Princess just has one frame, but I imagine that she could have more without much difficulty. Perhaps she’s already rigged to allow it.
Monsters have a few states of mind, such as peaceful or angry, and they can behave a bit differently depending on their frame of mind.
A specialized kind of Entity has only recently been added, the NPC (Non-Player Character). There’s only one current example, Horn Girl, who gives the Princess a small side quest. NPCs are given a bit more behavioral flexibility using a Finite State Machine object, FSM, and a moderately complex table of state transitions. This area should be considered a “work in progress”, as in fact the Horn Girl hasn’t even been completed. She promises to give the Princess a gift at the end of the side quest, but–I believe–she does not yet deliver.
There’s a moderate hierarchy around NPC, Monster, and Player. There is DungeonObject, which is the superclass NCP and Entity, and Entity is the superclass of Monster and Player. There is a bit of common code shared between Monster and Player, in Entity. NPC shares no significant code with Monster or Player. But she’s not done yet, so that design should be considered to be evolving.
I should point out that when I started mentioning NPC, I have started glancing at the code to remind myself how things work.
Tiles get involved in the movement of Entities. Whenever an Entity tries to move into a new tile, the tile is asked whether the move is allowed. Some tiles are walls, and always decline the offer. Floor tiles (or are they called room tiles, I don’t remember) check their contents. Monsters will not move onto a treasure or decor item. The Player generally can, and when she does she receives the item or interacts with it it other ways. (Decor can be harmful, for example, and may or may not contain a treasure.)
And, if a Monster tries to move to the Player’s tile, or vice versa, combat may take place.
The decision making, or most of it, is handled by the TileArbiter class, which is instantiated with the existing resident of the tile, and the entity trying to move in, and looks up behavior based on the type of resident and mover. I see here that there are many pairs considered, among Player, Monster, and at least Loot, Chest, Lever, Key, NPC, WayDown, and Spikes.
Among the things that can happen is the initiation of combat between Player and Monster. All the interactions are triggered by a message to the mover of the form startActionWithXXX
, where XXX is the type of thing, Loot, Monster, etc. If that method is to start combat, the GameRunner is informed to initiateCombatBetween
the Entities.
Combat is random, divided into CombatRound
occurrences. The game handles one round, with an attack and a possible riposte. The implementation of CombatRound is complicated and strange, due in part to the fact that we are scrolling the results, which means that the game knows the Monster is dead before the report comes out. So it has to ask questions like “will the monster be dead” before it decides to allow a counterattack, and so on. We’ll have to study that area in due time. Probably. Maybe.
Hm, what else? Tiles are stored in an instance of Dungeon, which can answer a wide range of questions that Entities need to know, and it manages the construction of dungeons, so it, too, has a very broad interface:
Dungeon should probably at least be divided into the part that makes a dungeon and the part that is used to report on and find tiles during game play. There may or may not be a clean break already available.
Oh, and there is of course Inventory
, the collection of treasures that the Player has accumulated. The Inventory can draw itself, as a row of items at the top of the screen. To use an inventory item, the player touches the item and that item’s action is taken, typically removing the item from inventory, as most items are one-use only. A common action is to improve one’s health, but items can in principle do anything. One of them even creates a cloud monster that will lead you to the WayDown, the entrance to the next lower level. You’ll need a Key to do that. And so on.
Those are most of the major and middle-sized objects. There are of course objects to read images and arrange them for objects to use, objects or structures used to define levels or monsters, and so on. We’ll stumble in and out of those as we look more deeply.
For Now
For now, that’s a good overview. With that much in mind, I have these feelings about the design:
GameRunner, Tile, and Dungeon seem quite large and surely can be partitioned somehow. Possibly some of that complexity can be moved to the publish-subscribe model. (The main object there is Bus, by the way.)
One big issue is simply the size of the thing. That’s mostly a limitation of Codea itself, which doesn’t attempt to be up to the modern standards of an IDE. It’s amazing that it does what it does. But 51 tabs is hard to manage, as are over 90 classes and nearly 900 methods.
I’ll think a bit over the next few days about what I “would do” if I were starting over, which may give some idea for what to push toward. Off the top of my head:
- Making App
- I would really like to have an app for the “game designer” to use to build the game, and an app that loads and plays the game. I’m not sure whether I’m up to it, nor whether it’s practical given how Codea works.
-
I’d like to have that app, or another one, do a better job of producing statistics and visible information about the application one is working on. Here again, it’s a matter mostly of time and inclination, and somewhat a question of what is practical.
- Separate Construction from Execution
- GameRunner and Dungeon, and some other classes, include code for creating the game or a level, and then for playing. You’d think those would be two separate classes, perhaps the one creating the other.
- Less Centralization
- I don’t like the fact that so much of the game is mediated by “brain” objects like GameRunner (and to a lesser extent, Dungeon). I would prefer that the Entities be programmed more like we imagine the game when we’re playing.
-
We find ourselves in a room in a dungeon. There are objects and creatures in the room. We can walk around the room, interact with an object or creature, or walk away and down a hallway to another room. The game should probably remain turn-based but even so, it seems like the Entities could just look at the tiles around them, mostly at the tile they’re moving into, and things would “just happen”.
-
I feel that it isn’t enough like that now, that GameRunner gets asked too many questions as does Dungeon and even Tile. I feel that there should be a better way. I am inclined to believe that publishing and subscribing to events would be better.
Right now, that’s what I see and what I think. Let’s talk about what to do about it.
How We’ll Proceed
- Refactor, not Rewrite
- The fundamental approach is given: we will refactor this program to improve its design. We will not create long-lived branches to do this work: we’ll go in small day-by-day steps wherever possible. We might rewrite little things, we might replace some objects. We might move some facilities outside: the Finite State Machine could be an example of something like that. We already have the fp functions off to the side.
- Low Interference
- We will work so as not to let the design changes get in the way of new implementations. (We might even do a couple just to prove the point, but if the tests all run every day, it’ll be pretty clear that we’re not in the way of new features.)
- Experiments
- We might do some separate design experiments. For example, we might build a small example of entities jointly operating on a plane without an intermediating “brain” object, to get a sense of what we like.
- What We Like
- That’s an important notion, worth a surprise heading. When we look at code5, we get ideas for how it might be better. Some of those ideas, we hope, are good ones. Some of them, we can be certain, are clunkers. Often if we try a clean sheet of paper prototype we can learn more rapidly and then if we like the idea, we work out how to evolve toward it in the real program.
- Small Improvements
- We will take many opportunities to do small improvements. These will at least increase our understanding, but they’ll likely also move us slightly in a better direction. Enough moving in the right direction and suddenly one more small change makes a big improvement in design. We’ll hope to find some of those.
- Thinking
- The real point of the exercise, of course, is thinking. I have no plans to create a real game here. We’re here to use this program as a thought experiment to give us a better sense of how and why to make improvements in real code, code that matters.
- Joy
- No. The real point, don’t tell anyone, is the sheer joy of performing our work. The feeling of smoothing out the rough bits in a thing we have built, of shifting it around until it is stronger, or clearer, or more flexible … that’s a good, joyful feeling, and if I can only attain one goal, it would be to open your eyes to the possibility of that joy in your work life.
I hope you’ll follow along.
-
cf Marx, G, not K. ↩
-
Yes, I am writing long and intricate sentences on purpose. I just feel like doing that. Stay calm, I almost always get to the end of the ↩
-
Thanks to Kent Beck for this word, which I will unashamedly adopt. Read his Tidy First? for a treat. ↩
-
Not that Better Place. Just a better place. ↩
-
Did you ever look at your code? I mean, really look at it? <inhales> ↩