Dungeon 230--I'm goin' in.
In a fit of madness, I decided to try to convert the square-tiled Dung game to use hex tiles. Let’s see how well my theory of programming holds up. Just inspection and planning today.
Despite decades of “learning” that to get a good program, we had to design it first, my additional decades of work in the world of Agile Software Development has exposed me to the ideas of my many betters in the business. Between that exposure and many years of “just trying things”, I’ve come to believe that we can produce a perfectly well-designed, often better-designed, program while working incrementally.
The basic idea is simple. We start with the ideal design for the current program, which does nothing. A null design, if you will. We start thinking about how our program might be designed, right at the beginning, and we never stop thinking about its design.
We then build one “feature”, implement one “story”. That will be pretty simple, especially if we slice our stories down to a few days’ work. Over those few days, we think of a simple design for our simple feature, and we implement the feature, using that design.
We refactor the code, removing duplication, putting functions where they belong.
When our story is done, we again have as close to a perfect design as we can make it.
Then we repeat. One story, improve the design, refactor toward perfection.
Sometimes we’ll get a story that teaches us what a better design would be than the one we have. We refactor in that direction. Since our existing design is as close to perfect as we can make it, that refactoring is usually fairly simple, mostly a matter of separating out some concerns and adding in some new ones, into some new class structure or other chunk.
It turns out that this approach works rather nicely. My many demos in Codea demonstrate that. I’ve even “surprised” my development team at the end of such programs, such as when I “demanded” that the Asteroids program needed better-looking asteroids, and retrofitted semi-attractive sprites into the program, with very little difficulty.
That trick wasn’t terribly difficult. You’d expect that any decent design would have the drawing of the asteroids broken out fairly nicely, so it should be a small matter of programming to change how they are drawn. No one was impressed.
Some people should have been impressed, because their programs back at work don’t have “obvious” capabilities broken out nicely. I fondly hope that seeing things divided up nicely gives them ideas for their own programs.
Just In Time
This is a sort of “just in time” approach to design. We enhance the design incrementally, improving it as we need it. If we can do this, we will likely go faster to any given feature set than with a larger “up front” design.
Why? Two reasons. First, we deliver our first feature in the first week, and more features follow on every few days, so we build up a large buffer of capability compared to the project that waits until the design is done to start. Second, our design is as well-fitted to the real problems as we can make it. As opposed to a speculative up-front design, which is never quite what we needed after all, ours is simpler, smoother, and as close to just right as we can make it. Third (I lied about the two), Since most features are implemented against a simple design, with only the last ones burdened by the full design library, our individual features are not only built sooner, they’re built faster.
You have to admit, it’s a hell of a story, if it’s true. And, up to this point, my experience, and that of my many colleagues, says that it is true. We can deliver a product sooner, that is just as well designed, using incremental design and development.
But what about surprises?
People first thinking about incremental design often raise this objection: But what if some surprise requirement comes out of the blue that our simple design can’t support? Wouldn’t it be better to have designed for that up front? Won’t we be dead in the water if there are surprises?
Yes, well, if it’s a surprise, why would we imagine that we’d cater for it in our up front design? And if we did think of it early on, couldn’t we just lean our incremental design a bit in that direction and still be better off?
Ancient History
I like to tell a story on myself, since I came out a winner. Long ago, when all this stuff was just getting started, we had a class member who was a CMMI assessor, one of those people who went around assessing your team as Level 1 or Level 3 or whatever, back when that was popular. These people are very expert in software and software process.
Two good things happened with this person in the class. First was, at the end of the class, they said “A team doing the things you teach here would be assessed at at least Level Three.”
So that was nice. But in further discussion they raised the issue of a surprise feature that an incremental design just couldn’t handle. I asked for an example, and they came up with this: “If you were writing a text editor and didn’t plan for Undo, you could never put it in at the end.” I think we argued that you could, with some handwaving.
Not long after that, I wrote Extreme Programming Adventures in C#, in which the whole massive book was me, writing a text editor in C#, a language that I didn’t know at the beginning of the book. The book is well thought of by both the people who bought it.
In that book, I “forgot” about undo, and if you were to read it, you would find no place in it where I set things up for undo. Every aspect of the code’s design is there because the code, right then, needs that aspect.
And at the end, I put in undo. It was easy. No sleight of hand. Of course I hadn’t forgotten the need for undo, and you could argue that that was enough to tilt the design to where undo would be easy enough. You could argue that, but you know how I write. I put down every thought into the writing. If you were to read that book–don’t, it’s very out of date–you wouldn’t spot any leaning toward undo.
So that was fun. What about now?
Hexagon Maps???
I can tell you that in the past 220 articles about the Dung program, I wasn’t thinking about hexagons. The game is relentlessly focused on square tiles. I started thinking about hexagons a week or so ago, when we were talking about doors, walls, and boundaries in our Tuesday evening “Friday Night Coding” ensemble.
And yesterday, someone typing on my iPad said that we’d try to make Dung run on hexagons.
I’m not afraid. After all, I can just start writing about something else. I am concerned, though, because while I really like the Tile design in Dung, I know that it is bearing a lot of weight, so it may be tricky to untangle it enough to support hexes.
That’s why I want to try it.
Here’s the story driving this change:
Dungeon games are no longer hot. Outdoor exploration games are hot, where you explore areas and maybe build houses and things. Those games don’t use square tiles. They use hexagon maps. Change our game to use a hexagon map instead of the square tiles.
We have some questions. We’re afraid even to ask the first one:
Do you want the game converted to hexes, or do you want a version that still uses squares? And what about a game that uses both?
We’re afraid to ask that because we know already what any product manager would say:
Excellent idea. Yes! Let’s have both. Maybe squares indoors and hexes outdoors. Well done, carry on!
Oh hell. I’ve thought of it. Now I have to try to do it. Why did I even come in here this morning? Let me at least take it as a stretch goal:
If possible, allow for a game with both square-tiled and hexagon-tiled levels. Maybe even a mixed level?
I’m burying myself here. I feel a massive crash and burn coming on. Well, I’ve been a laughing stock before. I’m sure we’ll learn something, if only not to open our big fat trap.
Let’s begin with some review of the D2 Dung program. I’ll make a duplicate of it, called DX. X for heX, of course.
Examining DX
It has been a while since I looked at this program, and since I don’t remember what I did yesterday, I certainly don’t remember everything about this Dung program that I started working on in December 2020.
The Game is run by the GameRunner class, which has a method createLevel
. That method uses a dungeon
to create a level.
function GameRunner:createLevel(count)
-- determine level
self.dungeonLevel = self.dungeonLevel + 1
if self.dungeonLevel > 4 then self.dungeonLevel = 4 end
self:dcPrepare()
-- customize rooms and connections
self.rooms = self.dungeon:createRandomRooms(count)
self.dungeon:connectRooms(self.rooms)
-- paint dungeon correctly
self.dungeon:convertEdgesToWalls()
-- ready for monsters
self.monsters = Monsters()
self:placePlayerInRoom1()
...
The dungeon
is an instance of Dungeon class, and its job is to hold onto tiles:
function Dungeon:init(runner)
self.runner = runner
-- declare unset member variables. needs improvement.
self.tiles = nil
self.tileCountX = nil
self.tileCountY = nil
end
function Dungeon:createTiles(tileCountX, tileCountY)
self.tileCountX = tileCountX
self.tileCountY = tileCountY
self.tiles = {}
for x = 1,tileCountX+1 do
self.tiles[x] = {}
for y = 1,tileCountY+1 do
local tile = Tile:edge(x,y, self.runner)
self:defineTile(tile)
end
end
return self.tiles
end
function Dungeon:defineTile(aTile)
assert(not TileLock, "attempt to set tile while locked")
local pos = aTile:pos()
self.tiles[pos.x][pos.y] = aTile
end
Dungeon has masses of utility methods. 39 total methods. It can provide a table of all the tiles closer to a given tile, or to the player, further away, or the same distance. It knows whether a tile is accessible (i.e. floor as opposed to wall or edge). It knows how to create rooms (rectangular areas of floor) and halls. It can return the neighbors of any tile. It knows the direction to move to get to any neighboring tile.
I should mention that I am not remembering any of this: I’m looking at the code and the names of the methods.
Hmmm …
This doesn’t seem too bad. If it were really true that all the geometry of the game play area (formerly called dungeon) is embedded in this one Dungeon class, then we “should” be able to plug some other kind of tile into it and Voila! we have a hex game.
However …
There’s the Tile class. That’s the class that draws the squares. That would have to be replaced with a HexTile kind of class. What does Tile know and do?
There are a bunch of little creation methods to make a hallway Tile or an edge Tile or a floor Tile. Fine, we’ll need ground and rocks and bushes. Same kind of deal.
There are sprites, little images that make the tiles look like something. No problem, our new tiles will need sprites too. Hex ones.
Tiles have contents, the treasures and such that reside on the tiles. I don’t remember whether the player and NPCs are in the contents or not. No matter, our new tiles will need contents as well.
A Tile can answer whether it will allow an entity to enter. It uses TileArbiter to decide, and TileArbiter, I think, is where the decision is made to attack.
Everything seems kind of OK so far, nothing square about the Tile yet.
This is interesting, and probably wrong:
function Tile:getSurroundingInfo()
local byte = 0
local ck = { vec2(1,1),vec2(0,1),vec2(-1,1), vec2(1,0),vec2(-1,0), vec2(1,-1),vec2(0,-1),vec2(-1,-1) }
for i,p in ipairs(ck) do
byte = byte<<1
local pos = self:pos() + p
local tile = self.runner:getTile(pos)
if tile:isFloor() then
byte = byte|1
end
end
return byte
end
This is the method that is used by the ? key, which will display information about all the tiles next to the player. Those vectors are of course the directions to the neighbors of the given tile. Much of this code probably should be being done in Dungeon. Wherever it’s done, the direction logic needs to be different for hexes. But that code, given our hex directions, would probably “just work”.
Tiles are involved in illumination. They are near the top of the illumination logic in fact. I don’t think our illumination is particularly strong but be that as it may we’ll surely need to illuminate our hex tiles. I know if I asked the Product Manager, they’d say “Good idea, yes!”
Current Reaction
Based on this much study, maybe an hour’s worth, I’m less nervous than I was. It seems that something like this would nearly work:
- New HexTile Class
- If we were to create a new class that drew hexagonal tiles, it almost seems like most of the code would sort of work. (For values of “almost”, “seems like” and “sort of’.)
- Dungeon Class
- The Dungeon class could create the new kind of tiles, and a lot of things might just work. (“Might”.) There’d have to be adjustments to the directions from one tile to another, dealing with the differing coordinates.
-
Room creation and hallway creation would be different. I’m sure we could create a nearly rectangular room of hexes if we wanted to. We’d surely want new schemes for making outdoorsish layouts but that’s just new work, not likely to break anything.
I’m starting to think this could work.
Very Tentative Plan
There’s one “main branchpoint” to consider. Do I just slam in the Hex tiles somehow and see what happens, or do I do a little prep work first?
I’ve only noticed one thing that seems to me to be out of place, and that’s the duplication of the directions to neighbors, which occurs separately in Dungeon and Tile. I think it belongs in Dungeon, and that dungeon is likely to forward back to the tile class to get the array of directions.
So it’s OK that Tile knows the directions, and not OK that Dungeon does. But Tile knows the directions in the wrong place, so it’ll need to be broken out into another little method. Could do that now, it’s so trivial.
Either way, I think the Tiles know the directions and Dungeon should probably use them. But we’d have to look at the code in detail to be sure.
This question comes to mind: how many references are there to Tile class? It turns out that other than in tests, there are very few. Maybe we can have two new classes, SquareTile and HexTile, and Tile becomes a class that contains the currently-active style, forwarding those calls to Square or Hex as needed.
Or to save a lot of renaming, maybe we have Tile meaning square, Hex meaning hex, and a new object MumbleSelector that knows which kind to create. Once they’re created, they’ll need to have the same protocol.
Opportunity for some kind of Strategy objects or something.
Less Tentative Plan
I think what we’ll try next time is to somehow set Tile aside and plug in Hex and see what explodes. That will give us a quick look at things that are major showstoppers, and things that will just need to be implemented as we move forward.
This might just work. And if not, we’ll know the reason why.
Keep watching!