Marketing makes an impossible demand. Can we make it possible? If not, does that mean incremental/iterative doesn’t work? Is ‘Agile’ wrong???

My marketing department (me) has asked the development team (me) to provide a capability that seems entirely counter to the design. I have to agree that it’s a good idea. But can we implement it without a major rewrite?

If we can’t, it’s a pretty strong indictment of my theory that if we keep our design clean and clear, we are as well-situated as we can be to respond to whatever needs may arise. If this isn’t possible, and reasonably straightforward, all of Agile Software Development could come crumbling down.

What is this new requirement?

Provide the ability to build specific dungeon layouts, so that level designers can specify all the rooms, hallways, monsters, and contents.

In discussion, some details came out.

Random levels are good
The random layouts are interesting, with their intricate hallways. We just want to have specific layouts in addition.
Better asset allocation
At some time, we may want to improve the random allocation of assets in the random levels.
Maze level?
We might even want a maze level at some point, probably one big maze room. We’re not sure: it might be boring. But don’t make it impossible.
Clock level?
Someone described a level with 12 rooms arranged roughly like the hours of a clock, where you had to adventure from room to room until finally the last room had the path to a big central boss room.
Rectangle level
Someone drew a 4x3 set of rectangles filling the whole tile space, and showed different doors between them, indicating that it would be possible to have many unique combinations of connections, which might be interesting.

The marketing and game design people (me) were so enthusiastic about this idea that the development team (me) didn’t want to rain on their parade. They had the development team (me) nodding along as they described things they could do with this new capability,

It’s now the product’s top priority.

How Can We Do This?

The good news is that level creation is broken out, at least into a separate method (and methods that it calls) that are dedicated to level creation. And we’ve been saying for a long time that level creation should probably be its own thing, not just part of GameRunner, which is rather a blob, although a moderately well-factored blob by virtue of our preference for small methods.

The flow of level creation is pretty clear, in this method:

function GameRunner:createLevel(count)
    self.dungeonLevel = self.dungeonLevel + 1
    if self.dungeonLevel > 4 then self.dungeonLevel = 4 end
    TileLock=false
    self:createTiles()
    self:clearLevel()
    self:createRandomRooms(count)
    self:connectRooms()
    self:convertEdgesToWalls()
    self:placePlayerInRoom1()
    self:placeWayDown()
    self:placeSpikes(5)
    self:setupMonsters(6)
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:createLoots(10)
    self:createDecor(30)
    self:createButtons()
    self.cofloater:runCrawl(self:initialCrawl(self.dungeonLevel))
    self:startTimers()
    self.playerCanMove = true
    TileLock = true
end

Some of this seems to be unique to all levels, like createTiles and what’s above it, and createButtons and what follows. The rest is a fairly good outline of what needs o go on.

Furthermore, when we think about the level’s layout, that’s all down to createRandomRooms and connectRooms. The first one arranges rooms in the tile space, and the second connects them.

As far as we (the whole team) can see, if we can control how the rooms are laid out, in what is now createRandomRooms, and how they connect, now handled by connectRooms, we can draw any room layout we want.

Maybe this isn’t as hard as we feared.

The team (me) decides that we’ll try a spike to create a different level. We choose one that will divide the whole space into a 4x3 layout, like that idea above, and connect the rooms in a kind of S curve. We’ll have just one layer of wall between rooms, since we think that will look best for that kind of a layout.

For our spike, we’ll just replace the create and connect methods. After that, we’ll figure out what we really should do.

We suspect that if we create the rooms correctly, the connect that we have ma do the job. We’ll see.

Let’s do some math. This is where we’ll get in trouble, I’m sure.

The tile space is 85 tiles wide by 64 tiles high. We’re going to have four rooms in width, and three in height. We need four walls vertically, and five horizontally. That leaves 80 tiles horizontally and 60 vertically. That gives us room for rooms that are 20 tiles wide, and 20 high, if my arithmetic is correct.

Those numbers come out so exact that I don’t trust them. Be that as it may, we should, if these numbers are correct, be able to “just” create rooms with corners starting at 2,2 and incrementing by 20. I don’t recall how we create rooms now.

function Room:init(x,y,w,h, runner, paint)
    if paint == nil then paint = true end
    self.x1 = x
    self.y1 = y
    self.x2 = x + w
    self.y2 = y + h
    self.runner = runner
    if paint then
        self:paint()
    end
end

Nice, they are corner-based. That’ll be handy.

The createRandomRooms looks like this:

function GameRunner:createRandomRooms(count)
    local r
    self.rooms = {}
    while count > 0 do
        count = count -1
        local timeout = 100
        local placed = false
        while not placed do
            timeout = timeout - 1
            r = Room:random(self.tileCountX,self.tileCountY, 4,13, self)
            if self:hasRoomFor(r) then
                placed = true
                table.insert(self.rooms,r)
                r:paint(#self.rooms)
            elseif timeout <= 0 then
                placed = true
            end
        end
    end
end

We can do something similar for our new function:

function GameRunner:createRectangleRooms()
    local r
    self.rooms = {}
    for i = 1,12 do
        r = self:createRectangleRoom(i)
        table.insert(self.rooms, r)
    end
end

This puts off the inevitable nicely. Still, we must get down to it:

As soon as I start writing createRectangeRoom I realize that rather than compute x and y from i, I can do the work above, so I recode:

function GameRunner:createRectangleRooms()
    local r
    self.rooms = {}
    for row = 1,3 do
        for col = 1,4 do
            r = self:createRectangleRoom(row,col)
            table.insert(self.rooms, r)
        end
    end
end

Much easier. Now:

function GameRunner:createRectangleRoom(row, col)
    local y = 2 + (row-1)*20
    local x = 2 + (col-1)*20
    return Room(x,y,20,20,self)
end

And I have to do the connect function, or go back and use the old one. Let’s create it on the grounds that we expressed it and we might want to change it.

function GameRunner:connectRectangleRooms()
    self:connectRooms()
end

I actually expect this to work, though there must be at least one mistake in there.

The tests don’t terminate. Dungeon Utilities. This is a spike, so I just turn off that set. I get the twing noise, but no dungeon appears. Bummer.

I need to find out where it is. It has gotten past our two new methods. I even printed out the room dimensions, and they seem to be OK.

I suspect some of the placement code must be looping. More prints, I guess.

Reading the code, I jump to a conclusion: placing spikes will take forever, because we have only 12 tiles that look like hallways in the whole space. I comment that out.

That brings up the picture, and it is amusing:

one big room

We have created one big room. Why? Because we used width and height of 20, which makes each room 21 wide. We need them to be less than 20, so we need size 18:

function GameRunner:createRectangleRoom(row, col)
    local y = 2 + (row-1)*20
    local x = 2 + (col-1)*20
    return Room(x,y,18,18,self)
end

The rooms are huge. Here’s the initial view:

initial

So the rooms are now right. The connections are odd. I expected them to go up to the top then over one then down, etc. The reality is that each row is connected left to right, and only the first column is connected top to bottom. I’m not terribly worried about that, since I just reused the existing method, but I am curious.

The point of a spike is to learn, so I think I’d like to learn what’s up with the connections.

I see one thing that I didn’t do. I didn’t paint the rooms with their room number. That could mess things up badly.

function GameRunner:createRectangleRoom(row, col)
    local y = 2 + (row-1)*20
    local x = 2 + (col-1)*20
    local r = Room(x,y,18,18,self)
    table.insert(self.rooms, r)
    r:paint(#self.rooms)
end

That gets things connected as I expected:

connected

I have noticed that the WayDown placement needs to be better controlled. Of course it’s just randomly “far away” at present.

But this time, I see a different connection of the rooms than last time:

different

Last time it was right, up, left, up, right. In the pic above, the leftmost room has two entrances, one right and one up. What’s up with that?

I change the tiny map code to always display the full map, and sure enough, every now and again, I get the other layout. What’s up with that? Surely the rooms are always created in the same order … are they connected in the same order? Looks like:

function GameRunner:connectRooms()
    local dungeon = self:getDungeon()
    for i,r in ipairs(self.rooms) do
        if i > 1 then
            r:connect(dungeon, self.rooms[i-1])
        end
    end
end

I resort to printing out the connections as they are made, and they are always correct, even when the odd connection appears. Finally, I realize what’s going on:

The rooms are numbered like this

9 10 11 12
5  6 7  8
1  2  3  4

I connect them in numeric order, so I connect 5 to 4. That can go one of two ways, randomly, either across and down (which makes the map what I expect) or down and across (which surprises me). I do need my own method:

function GameRunner:connectRectangleRooms()
    local connections = {1,2,3,4, 8,7,6,5, 9,10,11,12}
    local dungeon = self:getDungeon()
    for i,r in ipairs(connections) do
        if i > 1 then
            local r1 = self.rooms[i]
            local r2 = self.rooms[i-1]
            r1:connect(dungeon, r2)
        end
    end
end

I thought this resolved the issue but of course it’s not using the connection table correctly. It needs to be like this:

function GameRunner:connectRectangleRooms()
    local connections = {1,2,3,4, 8,7,6,5, 9,10,11,12}
    local dungeon = self:getDungeon()
    for i = 1,12 do
        if i > 1 then
            local r1 = self.rooms[connections[i]]
            local r2 = self.rooms[connections[i-1]]
            r1:connect(dungeon, r2)
        end
    end
end

There. I think that actually works.

What Have We Learned, Brain?

We have learned that we can essentially just replace the two methods createRandomRooms and connectRooms with functions that create any layout that we want.

I even turned the spikes back on and the game managed to find places for the five we request. That method selects random tiles until it finds a hallway tile that is enclosed on two sides and open on the other two. In our new rectangle layout, there are only 11 such tiles, but the delay wasn’t noticeable.

Of course, we already know we’ll want to do some tuning of these new rooms, so placing spikes, decor, loots, and monsters may all need tuning. I foresee no particular difficulty in that area.

We’ve learned that 20x20 rooms are too large. They are wide open and we can’t see their walls. It’s more irritating than scary, and it means that there’s a lot of wandering we have to do to see the edges. Even if we illuminated all the way out to the edges, there are only 16 tiles vertically on the screen, so a height of 20 means that the top and bottom walls are off screen when we’re centered. This will be useful feedback to level designers (me, and anyone who wants to send me a picture). Rooms can be adjacent, but if they are too large it gets weird.

We have found that, as we hoped, everything about the dungeon creation is fairly nicely wrapped in the methods that createLevel calls. That tells me that we can probably very readily pull all that out into a separate dungeon building facility, which will serve as a good spot to plug in the new dungeon ideas if the product design people (me) ever come up with something.

Bottom Line

Bottom line, Agile is safe. Our “reasonably clean and clear design” shows itself sufficient to readily plug canned dungeon designs into our structure. It should be easy enough to build a little language to do it, some kind of simple file format, maybe YAML or JSON1 or a tune of our own invention, should that be desirable. As it stands, we can code up a new design pretty readily.

I’m going to revert the code. What we did was simple and I can get it back if I need it. I’m not even going to commit it to a branch. Just gone.

Net, net, net

A good morning. A seemingly difficult problem, and the code to solve it will fit in with little difficulty. Just what we want.


D2.zip

  1. No, I don’t really think JSON is a “simple file format”, but it’s at least bearable.