Dungeon 262
Plotter or Pantser? Which am I? Which are you?
In writing, especially fiction writing, a “Plotter” is a person who plans their novel, creating detailed outlines and other materials, working out the shape of the story before writing it. In contrast is the “Pantser”, who “flies by the seat of their pants”, letting the characters tell the story, planning very little.
Now I am not one to believe that binary distinctions really describe the world well. In fact, I think that no finite number of distinctions is likely to work. I’m looking at you, MBTI. Living things, as most of us here happen to be, have widely varied behavior across time and space. Glib explanations like “he’s an INTJ, of course he’d say that”, or “he’s a Pantser, of course he’d make that choice” are likely to lead us down a path of error, mistakes, and blunders.
Still, the Plotter/Pantser notion can be useful. One of my many betters, Martin Fowler, has identified over 60 different refactorings. You can see the catalog at refactoring.com, and I heartily recommend the book, as it includes detailed steps for accomplishing some very complex code transformations.
A Plotter in refactoring would likely use Martin’s book, or their own personal catalog, as a basis for improving code. They would likely plan their moves to accomplish the design improvement they have in mind, and then follow that plan as closely as practical.
Naturally, in real code, the pre-planned approach sometimes runs into issues in the code that force a deviation from the plan. I imagine the same thing can happen in the outline of one’s novel, where we didn’t notice that Jason was seriously injured three chapters ago and probably cannot run wildly to Jenny when he sees her on the dock. With luck, we see the problem as we write, and fix it. But, by and large, we can probably get a well-organized book by careful outlining, and we can probably get a significant refactoring to go pretty smoothly by planning it Plotter style.
If you were looking for a Pantser in refactoring, you would need to look no further than these pages. Owing, probably, to defects in my mind, I program in a very intuitive, follow my nose fashion. I think I always did, but my experience with “Agile” has driven me further in that direction.
There are at least two reasons beyond personal inclination that drive me to work in the Pantser style.
First, much–I’d argue most–of the code we encounter is not terribly well organized. It generally doesn’t match any particular well-known pattern. Even if we wanted to move the code so that it did match some desirable pattern, our starting position will probably be decidedly non-standard.
Second, I try to work at a relatively basic level, doing things that most anyone can do. I don’t feel write looking at some mess of code and then suddenly announce “This is a job for Captain Marvel” and then do something that most people have never heard of.
Third–I was mistaken about “two”–I believe that as we refactor real code, we’ll quite often go in the wrong direction, and I want us all to experience that that’s OK, we can always revert or just start digging in a new direction. Planning less than might be desirable helps ensure that we’ll encounter trouble. Probably more than I really need …
Fourth–I was way off with that “two”–I think there is great value in building up our “intuition” about the code, the sense that these things are kind of the same and belong together, and these things are different and belong apart, or that this thing changes faster than that thing, so they probably should be separated.
That said, most of Martin’s refactorings are pretty simple, and I’d probably do better to at least mention them as we refactor. I couldn’t possibly remember over 60 names, but I could remember a few. And the names are actually mostly generated over a very small space of nouns and verbs. They’re definitely worth studying, in the on-line list, and even better in the book. And I’ll try to do better at expressing where I think I’m going.
Be that as it may, it’s fair to say that I am very much a Pantser in how I work, and that if you’re more of a Plotter, more power to you. You’ll surely benefit from a bit more planning than I do.
Let’s review our rather limited plan, in the light of the code.
Ideas
We still have three leftover idea cards, I think:
- dcPrepare: move non-Dungeon stuff back to GameRunner.
- placePlayer RUNNER callback? Ditto WayDown. Return values?
- Should Monsters know RUNNER, and avoid parameters on method calls?
I should add a card. Yesterday, when I renamed some method, I had to change some tests and working code, because some of the other dungeon builds were relying on the method, not just the code moved to DungeonBuilder. And I think I ran into that difficulty a few days prior as well.
Right now, we have most of the building of the everyday game dungeon level moved out of GameRunner. There are other dungeon builds still in there, such as createTestLevel
and createLearningLevel
. Rather than start moving code from Dungeon to somewhere else, maybe we’d do better to finish moving all the build stuff out of GameRunner.
There are arguments on all sides. The way things are isn’t really bothering anyone. But the job’s not done until everything is moved out. But the job is never done, so maybe moving the others out isn’t that important. But if we don’t move them now, maybe the changes we make to Dungeon will make it harder to move things later. But that seems like it’s unlikely, since we’ll have had to make them work anyway. But if we move them, we can remove duplication and make future work easier …
Time to flip a coin and see whether we like how it comes up.
What’s Easier?
Moving the smaller creates over to DungeonBuilder seems easier to me, and I am feeling that I need something relatively easy to do. I actually have a theory about that.
When faced with a largeish thing to so, we can usually see some of the steps to doing it. Is it better to start with a hard step, or an easy one? I don’t think there’s an officially right answer. Sometimes I like to start with the hard thing, on the grounds that if I do a lot of work and then can’t do the hard thing, the work will be wasted. Other times I start with an easy thing, because when it’s done, the rest of the problem will probably look simpler.
But another rule I try to follow is to scale the work I undertake to the energy and brainpower I seem to have available. As such, it behooves me to do a small thing, today, and every day.
This is another way of looking at GeePaw Hill’s “Many More Much Smaller Steps” notion. Fact is, my experience is that I always do better when I’m taking very small steps, and that when I can make things easy, it will pay off immediately, in that I’m less likely to make a mistake, and when I do make a mistake, it’ll be easier to find.
Accordingly:
Move Creation Methods to DungeonBuilder
Here’s what we have now, for learning and test levels:
function GameRunner:createLearningLevel()
self.dungeonLevel = 99
self:defineDungeonBuilder()
self:dcPrepare()
-- customize rooms and connections
self:createLearningRooms()
self:connectLearningRooms()
-- paint dungeon correctly
self.dungeon:convertEdgesToWalls()
self:placePlayerInRoom1()
-- customize contents
self:placeWayDown() -- needs to be fixed room
-- prepare crawl
self.cofloater:startCrawl()
Announcer:sayAndSave(self:crawlMessages(self.dungeonLevel))
self:startTimers()
self:dcFinalize()
end
function GameRunner:createTestLevel(count)
self.dungeonLevel = self.dungeonLevel + 1
if self.dungeonLevel > 4 then self.dungeonLevel = 4 end
self:defineDungeonBuilder()
self:dcPrepare()
-- customize for level
self.dungeon:createRandomRooms(count)
self.dungeon:connectRooms(self:getRooms())
self.dungeon:convertEdgesToWalls()
self:dcFinalize()
end
Looking at those, it’s pretty clear which is easier to do.
I’m going to do this in a very Pantser way. I’m going to change the method in GameRunner to look very similar to this one:
function GameRunner:createLevel(count)
self:defineDungeonBuilder()
self.builder:buildLevel(count)
self:prepareToRun()
end
Then I’m going to move the old method over to DungeonBuilder, and run to see what explodes. I might look around a bit over there first, to see what can be scavenged.
And in fact I’m going to do the move first, of course, because I want a method that’s just like the GameRunner one over in the builder. Here it is, with renaming:
function DungeonBuilder:buildTestLevel(roomCount)
self.dungeonLevel = self.dungeonLevel + 1
if self.dungeonLevel > 4 then self.dungeonLevel = 4 end
self:defineDungeonBuilder()
self:dcPrepare()
-- customize for level
self.dungeon:createRandomRooms(roomCount)
self.dungeon:connectRooms(self:getRooms())
self.dungeon:convertEdgesToWalls()
self:dcFinalize()
end
Now the caller, modeled after createLevel
:
function GameRunner:createTestLevel(roomCount)
self:defineDungeonBuilder()
self.builder:buildTestLevel(roomCount)
-- self:prepareToRun() -- probably not needed
end
OK back to DungeonBuilder to see what we can change to give this code a chance. I’ll model after the buildLevel
there:
function DungeonBuilder:buildLevel(roomCount)
self:dcPrepare()
self:defineDungeonLayout(roomCount)
self:placePlayer()
self:customizeContents()
self:dcFinalize()
end
I’ll make the first part of the new method look the same:
function DungeonBuilder:buildTestLevel(roomCount)
self:dcPrepare()
-- customize for level
self.dungeon:createRandomRooms(roomCount)
self.dungeon:connectRooms(self:getRooms())
self.dungeon:convertEdgesToWalls()
self:dcFinalize()
end
Well this is interesting isn’t it? What does our defineDungeonLayout
do now?
function DungeonBuilder:defineDungeonLayout(count)
-- customize rooms and connections
self.dungeon:createRandomRooms(count)
self.dungeon:connectRooms(self:getRooms())
-- paint dungeon correctly
self.dungeon:convertEdgesToWalls()
end
Sweet. I think we can probably just call that.
function DungeonBuilder:buildTestLevel(roomCount)
self:dcPrepare()
self:defineDungeonLayout(roomCount)
self:dcFinalize()
end
Test. I feel fairly good about this. Woot! The tests all run. Commit: move createTestLevel work to DungeonBuilder:buildTestLevel.
That went smoothly, The next one might be a bit more difficult.
Move LearningLevel
The learning level code includes more than we’ve looked at this morning:
function GameRunner:createLearningRooms()
local w = 12
local h = 8
local a2 = { "In this room, there is a health power-up.",
"Please walk next to it (not onto it)",
"and press the ?? button.",
"You'll get an informative message about whatever you're next to." }
local t = {
{2,2, w,h},
{15,2, w,h, a2},
{28,2, w,h},
{41,2, w,h},
{2,11, w,h},
{15,11, w,h},
{28,11, w,h},
{41,11, w,h},
{2,20, w,h},
{15,20, w,h},
{28,20, w,h},
{41,20, w,h},
}
for i,r in ipairs(t) do
r[1] = r[1]+24
r[2] = r[2]+24
end
self.dungeon:createRoomsFromXYWHA(t, self)
local r2 = self:getRooms()[2]
local lootTile = r2:tileAt(self.dungeon, 2,2)
local loot = Loot(lootTile, "health", 5,5)
loot:setMessage("This is a Health Power-up of 5 points.\nStep onto it to add it to your inventory.")
end
function GameRunner:connectLearningRooms()
local dungeon = self:getDungeon()
local rooms = self:getRooms()
rooms[2]:connect(dungeon, rooms[1])
rooms[3]:connect(dungeon, rooms[2])
rooms[4]:connect(dungeon, rooms[3])
rooms[8]:connect(dungeon, rooms[4])
rooms[12]:connect(dungeon, rooms[8])
rooms[11]:connect(dungeon, rooms[12])
rooms[10]:connect(dungeon, rooms[11])
rooms[9]:connect(dungeon, rooms[10])
rooms[5]:connect(dungeon, rooms[9])
rooms[6]:connect(dungeon, rooms[5])
rooms[7]:connect(dungeon, rooms[6])
end
function GameRunner:createLearningLevel()
self.dungeonLevel = 99
self:defineDungeonBuilder()
self:dcPrepare()
-- customize rooms and connections
self:createLearningRooms()
self:connectLearningRooms()
-- paint dungeon correctly
self.dungeon:convertEdgesToWalls()
self:placePlayerInRoom1()
-- customize contents
self:placeWayDown() -- needs to be fixed room
-- prepare crawl
self.cofloater:startCrawl()
Announcer:sayAndSave(self:crawlMessages(self.dungeonLevel))
self:startTimers()
self:dcFinalize()
end
Still, this doesn’t look difficult, though it’s probably just detailed enough to cause me to break something. We’ll see.
First most all those methods over to DungeonBuilder. I even remembered to change all the class names to DungeonBuilder. See how clever I am?
Now delete the two helpers from GameRunner and edit the create.
function GameRunner:createLearningLevel()
self:defineDungeonBuilder()
self.builder:buildLearningLevel()
self:prepareToRun()
end
Naively, we’d do this. But I note that we set the level to 99 in the learning level. Is this important? I don’t know. Let me add a reminder to the code to look into it:
function GameRunner:createLearningLevel()
-- learning level is level 99? deal with this?
self:defineDungeonBuilder()
self.builder:buildLearningLevel()
self:prepareToRun()
end
I could make a card for this instead. Let’s try this.
OK now back to DungeonBuilder, rename the create method:
function DungeonBuilder:buildLearningLevel()
self:dcPrepare()
-- customize rooms and connections
self:createLearningRooms()
self:connectLearningRooms()
-- paint dungeon correctly
self.dungeon:convertEdgesToWalls()
self:placePlayer()
-- customize contents
self:placeWayDown() -- needs to be fixed room
self:dcFinalize()
end
I’ve taken a pass through to make it look like the main buildLevel. Let’s test it.
Room:109: attempt to call a nil value (method 'getTile')
stack traceback:
Room:109: in method 'paint'
Room:29: in field 'init'
... false
end
setmetatable(c, mt)
return c
end:24: in function <... false
end
setmetatable(c, mt)
return c
end:20>
(...tail calls...)
Dungeon:208: in local 'f'
fp:105: in field 'reduce'
Dungeon:210: in method 'createRoomsFromXYWHA'
DungeonBuilder:90: in method 'createLearningRooms'
DungeonBuilder:117: in method 'buildLearningLevel'
GameRunner:42: in method 'createLearningLevel'
Player:168: in field '?'
Button:79: in method 'performCommand'
Button:64: in method 'touchEnded'
Main:118: in function 'touched'
With luck that’s an obvious translation issue. I didn’t even look at those helper methods. Looks like the problem is in that XYWHA method:
function Dungeon:createRoomsFromXYWHA(roomTable, runner)
local makeRoom = function(result,roomDef)
return result + Room:fromXYWHA(roomDef, runner)
end
self.rooms = ar.reduce(roomTable, Array{}, makeRoom) -- 210
end
This is a bit scary. I’m not even sure that building the Learning Level worked before these changes.
I almost wish I were on a branch. Or had stash. But I don’t. I’ll commit, pull the previous version and test. Then I’ll decide whether to remove this commit.
OK, placing the WayDown failed, but the XYWHA stuff didn’t. So the problem we’re seeing now is a result of this morning’s changes. I’ll go back to the broken commit and carry on.
I’m sure there is a more graceful way to do that, if I had full git capability here, but I’m not sure what the right WorkingCopy plan was. Fact is, right now, the build is broken. I could remove this commit and start over but I don’t see the point. Let’s find the problem and fix it.
Room:109: attempt to call a nil value (method 'getTile')
stack traceback:
Room:109: in method 'paint'
Room:29: in field 'init'
... false
end
setmetatable(c, mt)
return c
end:24: in function <... false
end
setmetatable(c, mt)
return c
end:20>
(...tail calls...)
Dungeon:208: in local 'f'
fp:105: in field 'reduce'
Dungeon:210: in method 'createRoomsFromXYWHA'
DungeonBuilder:90: in method 'createLearningRooms'
DungeonBuilder:117: in method 'buildLearningLevel'
GameRunner:42: in method 'createLearningLevel'
Player:168: in field '?'
Button:79: in method 'performCommand'
Button:64: in method 'touchEnded'
Main:118: in function 'touched'
This rather messy report isolates what’s going on to the reduce, but there’s lots inside:
We see that this method wants runner:
function Dungeon:createRoomsFromXYWHA(roomTable, runner)
local makeRoom = function(result,roomDef)
return result + Room:fromXYWHA(roomDef, runner)
end
self.rooms = ar.reduce(roomTable, Array{}, makeRoom)
end
But we’re calling it with self
since we just moved the method over. This might be easy after all.
function DungeonBuilder:createLearningRooms()
...
self.dungeon:createRoomsFromXYWHA(t, self.RUNNER)
local r2 = self:getRooms()[2]
local lootTile = r2:tileAt(self.dungeon, 2,2)
local loot = Loot(lootTile, "health", 5,5)
loot:setMessage("This is a Health Power-up of 5 points.\nStep onto it to add it to your inventory.")
end
Test, hopefully.
DungeonBuilder:98: attempt to call a nil value (method 'getDungeon')
stack traceback:
DungeonBuilder:98: in method 'connectLearningRooms'
DungeonBuilder:118: in method 'buildLearningLevel'
GameRunner:42: in method 'createLearningLevel'
Player:168: in field '?'
Button:79: in method 'performCommand'
Button:64: in method 'touchEnded'
Main:118: in function 'touched'
Sure.
function DungeonBuilder:connectLearningRooms()
local dungeon = self:getDungeon()
local rooms = self:getRooms()
rooms[2]:connect(dungeon, rooms[1])
rooms[3]:connect(dungeon, rooms[2])
rooms[4]:connect(dungeon, rooms[3])
rooms[8]:connect(dungeon, rooms[4])
rooms[12]:connect(dungeon, rooms[8])
rooms[11]:connect(dungeon, rooms[12])
rooms[10]:connect(dungeon, rooms[11])
rooms[9]:connect(dungeon, rooms[10])
rooms[5]:connect(dungeon, rooms[9])
rooms[6]:connect(dungeon, rooms[5])
rooms[7]:connect(dungeon, rooms[6])
end
Change that first line to self.dungeon.
And the tests run and the LearningLevel works. (The WayDown is not properly placed, but that’s what that coment was about. That’s jut not done yet.)
Commit: LearningLevel creation now done in DungeonBuilder.
And Now For Something …
Now that we have all the real level building moved over, let’s see if there is any duplication that we should be concerned about
As a Pantser, I rely on detection of things like duplicated code to trigger me into improving things.
function DungeonBuilder:buildLearningLevel()
self:dcPrepare()
self:createLearningRooms()
self:connectLearningRooms()
self.dungeon:convertEdgesToWalls()
self:placePlayer()
-- customize contents
self:placeWayDown() -- needs to be fixed room
self:dcFinalize()
end
Compare to the canonical `buildLevel:
function DungeonBuilder:buildLevel(roomCount)
self:dcPrepare()
self:defineDungeonLayout(roomCount)
self:placePlayer()
self:customizeContents()
self:dcFinalize()
end
When we look at defineDungeonLayout
we find this:
function DungeonBuilder:defineDungeonLayout(count)
self.dungeon:createRandomRooms(count)
self.dungeon:connectRooms(self:getRooms())
self.dungeon:convertEdgesToWalls()
end
This drives me to say this:
function DungeonBuilder:buildLearningLevel()
self:dcPrepare()
self:defineLearningLayout()
self:placePlayer()
-- customize contents
self:placeWayDown() -- needs to be fixed room
self:dcFinalize()
end
function DungeonBuilder:defineLearningLayout()
self:createLearningRooms()
self:connectLearningRooms()
self.dungeon:convertEdgesToWalls()
end
And then this:
function DungeonBuilder:buildLearningLevel()
self:dcPrepare()
self:defineLearningLayout()
self:placePlayer()
self:customizeLearningContents()
self:dcFinalize()
end
function DungeonBuilder:customizeLearningContents()
self:placeWayDown() -- needs to be placed in fixed room
end
This is nice. The methods follow the same pattern. There is, however some duplication. Let’s commit and then decide whether to deal with it.
Test. Commit: tidy up DungeonBuilder to create duplication
Ooo, I detected a bug! When we create the Learning Level, its startup message isn’t the one about learning, it is the one that you’d get if you advanced to the next level in the regular game. Remember that 99? It was important.
Let’s fix that.
function GameRunner:createLearningLevel()
-- learning level is level 99? deal with this?
self:defineDungeonBuilder()
self.builder:buildLearningLevel()
self:prepareToRun()
end
function GameRunner:createLevel(roomCount)
self:defineDungeonBuilder()
self.builder:buildLevel(roomCount)
self:prepareToRun()
end
function GameRunner:defineDungeonBuilder()
self.dungeon = Dungeon(self, self.tileCountX, self.tileCountY)
local numberOfMonsters = 6
-- determine level
self.dungeonLevel = self.dungeonLevel + 1
if self.dungeonLevel > 4 then self.dungeonLevel = 4 end
self.builder = DungeonBuilder(self,self.dungeon, self.dungeonLevel, self.playerRoom, numberOfMonsters)
end
Let’s add an optional parameter to defineDungeonBuilder:
function GameRunner:defineDungeonBuilder(providedLevel)
self.dungeon = Dungeon(self, self.tileCountX, self.tileCountY)
local numberOfMonsters = 6
-- determine level
self.dungeonLevel = providedLevel or self.dungeonLevel + 1
if self.dungeonLevel > 4 then self.dungeonLevel = 4 end
self.builder = DungeonBuilder(self,self.dungeon, self.dungeonLevel, self.playerRoom, numberOfMonsters)
end
Now we provide the level:
function GameRunner:createLearningLevel()
self:defineDungeonBuilder(99)
self.builder:buildLearningLevel()
self:prepareToRun()
end
Test. Didn’t work. It couldn’t possibly work. Bad programmer, trying to be clever.
function GameRunner:defineDungeonBuilder(providedLevel)
if providedLevel then
self.dungeonLevel = providedLevel
else
self.dungeonLevel = self.dungeonLevel + 1
if self.dungeonLevel > 4 then self.dungeonLevel = 4 end
end
self.dungeon = Dungeon(self, self.tileCountX, self.tileCountY)
local numberOfMonsters = 6
self.builder = DungeonBuilder(self,self.dungeon, self.dungeonLevel, self.playerRoom, numberOfMonsters)
end
That works. Commit: Learning Level now has correct starting crawl.
I think that’s a nice stopping point. Let’s sum up.
Summary
We chose an easy path this morning, moving the remaining learning and test level creation over into DungeonBuilder. Now the only thing left is the Hex level, and I’m refusing to look at that. Truth is, it should be easy to move over, I am just resisting because I don’t like working on the Hex level.
Moving the levels over went very smoothly, There was a small glitch here and there, notably when I failed to convert a method’s use of self
to self.RUNNER
. That confused me, and I wondered whether it had worked before. If I hadn’t been so ready to revert, I’d have found the bug and moved on. Overall, though, I think I am too reluctant to revert, so rolling back too soon is a nice change.
If I were more adept with WorkingCopy, or had a real git client, I might have done that even more smoothly, but it went just fine. Backed up, checked, went back to head and carried on.
Referring back to Martin’s refactorings, most of what we did today was Move Method. Toward the end, we did an Extract Method to pull out the elements of the learning level creation that matched the shape of the primary level creation. Along the way in doing this whole refactoring, we’ve used Move Statements into Function and Move Statements into Callers, to get things into the classes where we want them.
A while back we did Replace Magic Literal. We have plenty of places to use that again, such as that 99 we just used.
Probably we used even more of Martin’s named refactorings. See whether you can identify other moves that I’ve made without naming them.
And stop by next time. I’m curious to find out what I do, and hope that you are as well.