Dungeon 26
Let’s get some rooms up in this ditch.
I think room digging and hallway digging are working now, so it’s time to create random dungeons and see what we get. To accomplish that I think I’ll need most of these things:
- Allocate a suitable size space for our rooms;
- Create a random room that doesn’t overlap any others;
- Create some number, probably around 30 random rooms;
- Connect the rooms.
We’ll need far more than that to make a game, including things like zooming in on the current room, putting a player, treasure, monsters, and so on in the rooms, moving the player around. It goes on and on, until we’ve learned what we want to learn, and had enough fun to want to move on. But for now, building the dungeon should be sufficient to the day, maybe even to more than a day.
From the D1 version of this program, in the Rectangle class, we had this:
function Rectangle:intersects(aRectangle)
x1lo, y1lo, x1hi, y1hi = self:corners()
x2lo, y2lo, x2hi, y2hi = aRectangle:corners()
if y1lo > y2hi or y2lo > y1hi then return false end
if x1lo > x2hi or x2lo > x1hi then return false end
return true
end
I reckon we’ll need that, and I see no reason right now not to make it part of room. What about testing? I could argue that this is code from a tested library. Or I could import the tests, because we did do a few. I’m going with the library excuse for now, let’s see if I regret it. And I will consider adapting the tests when I need something kind of easy to do.
So I add these:
function Room:intersects(aRoom)
x1lo, y1lo, x1hi, y1hi = self:corners()
x2lo, y2lo, x2hi, y2hi = aRoom:corners()
if y1lo > y2hi or y2lo > y1hi then return false end
if x1lo > x2hi or x2lo > x1hi then return false end
return true
end
function Room:corners()
return self.x1,self.y1,self.x2,self.y2
end
Now I need a random room. I think it will want to know something about the dimensions of the space, and the ranges of its width and height. I think I’ll write a test for this just to drive out some sense of how it’ll be used.
… No. I just don’t see a decent test to write. I may regret this. I’m just going to code createRandomRooms into GameRunner.
OK, I just kind of emitted this:
function GameRunner:createRandomRooms(count)
self.rooms = {}
while count > 0 do
local timeout = 100
local placed = false
while not placed do
timeout = timeout - 1
local r = Room:random(60,60, 4,15)
if self:hasRoomFor(r) then
placed = true
table.insert(self.rooms,r)
elseif timeout <= 0 then
placed = true
end
end
end
end
My plan is that we’ll loop until we’ve placed count rooms or timed out count times. This is a bit odd but it’s what I came up with. TDD might have made this better, but I’m sinning this morning. After all, we’ve only thrown this program away once …
Now I need Room:random
and GameRunner:hasRoomFor
.
function Room:random(spaceX, spaceY, minSize, maxSize, runner)
local w = math.random(minSize,maxSize)
local h = math.random(minSize,maxSize)
local x = math.random(0,spaceX-w)
local y = math.random(0,spaceY-h)
return Room(x,y,w,h,runner)
end
I noticed that I need the runner, so I’ll add that to the call beofre I forget:
local r = Room:random(60,60, 4,15, self)
Now the hasRoomFor
:
function GameRunner:hasRoomFor(aRoom)
for i,room in ipairs(self.rooms) do
if room:intersects(aRoom) then return false end
end
return true
end
I have to just call this now to see what it does.
GameRunner:78: attempt to index a nil value (field '?')
stack traceback:
GameRunner:78: in method 'setTile'
Room:25: 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...)
GameRunner:26: in method 'createRandomRooms'
Main:28: in function 'setup'
Well that’s interesting. Here’s what displays:
So the rooms are all on top of each other. The intersection code is failing somehow. And am I counting down count? I am not. So:
function GameRunner:createRandomRooms(count)
self.rooms = {}
while count > 0 do
count = count -1
local timeout = 100
local placed = false
while not placed do
timeout = timeout - 1
local r = Room:random(60,60, 4,15, self)
if self:hasRoomFor(r) then
placed = true
table.insert(self.rooms,r)
elseif timeout <= 0 then
placed = true
end
end
end
end
So what’s that message about? GameRunner 78 from Room init 25:
function GameRunner:setTile(aTile)
self.tiles[aTile.x][aTile.y] = aTile
end
Seems like tile x or y may be nil? Let’s review:
function Room:random(spaceX, spaceY, minSize, maxSize, runner)
local w = math.random(minSize,maxSize)
local h = math.random(minSize,maxSize)
local x = math.random(0,spaceX-w)
local y = math.random(0,spaceY-h)
return Room(x,y,w,h,runner)
end
function Room:init(x,y,w,h, runner)
self.x1 = x
self.y1 = y
self.x2 = x + w
self.y2 = y + h
self.runner = runner
-- leave wall on all four sides
for x = self.x1,self.x2 do
for y = self.y1,self.y2 do
runner:setTile(self:correctTile(x,y))
end
end
end
We’re creating with parms 60,60,4,15. I’m not seeing at all why we’d be coming up with nil x or y here.
Time for a test, isn’t it? Past time, my better side might say.
_:test("random rooms", function()
local x1,y1,x2,y2
local runner = GameRunner()
for i = 1,100 do
local r = Room:random(60,60, 4,15, runner)
x1,y1,x2,y2 = r:corners()
_:expect(x1>=0 and x1<60).is(true)
_:expect(y1>=0 and y1<60).is(true)
_:expect(x2>=0 and x2<60).is(true)
_:expect(y2>=0 and y2<60).is(true)
end
end)
I just rolled up a bunch of them. And got this:
6: random rooms -- GameRunner:79: attempt to index a nil value (field '?')
It’s not happening all the time. I think I know what’s up. I think we’re indexing address 60 in the array. I think I need to be a bit more conservative in where the rooms can go:
function Room:random(spaceX, spaceY, minSize, maxSize, runner)
local w = math.random(minSize,maxSize)
local h = math.random(minSize,maxSize)
local x = math.random(0,spaceX-w-1)
local y = math.random(0,spaceY-h-1)
return Room(x,y,w,h,runner)
end
If I’m right, the error will go away. It doesn’t. I’ll put an assert in that code in GameRunner.
function GameRunner:setTile(aTile)
local x,y = aTile.x, aTile.y
assert(x<60 and y< 60, "too big")
self.tiles[aTile.x][aTile.y] = aTile
end
OK, this is asserting. Let’s actually print the room if it’s bad.
Oh my. Here’s one important issue:
function Room:init(x,y,w,h, runner)
self.x1 = x
self.y1 = y
self.x2 = x + w
self.y2 = y + h
self.runner = runner
-- leave wall on all four sides
for x = self.x1,self.x2 do
for y = self.y1,self.y2 do
runner:setTile(self:correctTile(x,y))
end
end
end
We paint the room before we check it. That will no longer do. However, let’s leave that for now and do some checking on the rooms before we paint.
I freely state for your edification that I am a bit confused about what is happening. Oh. Wait. 60 is a legal index. 0 is not.
function Room:random(spaceX, spaceY, minSize, maxSize, runner)
local w = math.random(minSize,maxSize)
local h = math.random(minSize,maxSize)
local x = math.random(1,spaceX-w)
local y = math.random(1,spaceY-h)
return Room(x,y,w,h,runner)
end
Now I expect somewhat better behavior. OK, I’m not getting the crash any more but my test sometimes fails. This will get messy with all those expects going by bur we’ll let that ride for now: Ah: the test had my zero-indexing mindset engaged as well. Revised as below it runs fine.
_:test("random rooms", function()
local x1,y1,x2,y2
local runner = GameRunner()
for i = 1,100 do
local r = Room:random(60,60, 4,15, runner)
x1,y1,x2,y2 = r:corners()
_:expect(x1>0 and x1<=60).is(true)
_:expect(y1>0 and y1<=60).is(true)
_:expect(x2>0 and x2<=60).is(true)
_:expect(y2>0 and y2<=60).is(true)
end
end)
I don’t see much reason to keep it however. Now can I test on the screen? No, not yet. The rooms still paint while we aren’t sure they are good. Let’s have them optionally paint:
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
function Room:paint()
-- leave wall on all four sides
for x = self.x1,self.x2 do
for y = self.y1,self.y2 do
self.runner:setTile(self:correctTile(x,y))
end
end
end
Now we can do this in the random thingie:
function GameRunner:createRandomRooms(count)
self.rooms = {}
while count > 0 do
count = count -1
local timeout = 100
local placed = false
while not placed do
timeout = timeout - 1
local r = Room:random(60,60, 4,15, self, false)
if self:hasRoomFor(r) then
placed = true
r:paint()
table.insert(self.rooms,r)
elseif timeout <= 0 then
placed = true
end
end
end
end
This took me a time or two to get the flag right. It goes like this:
function GameRunner:createRandomRooms(count)
self.rooms = {}
while count > 0 do
count = count -1
local timeout = 100
local placed = false
while not placed do
timeout = timeout - 1
local r = Room:random(60,60, 4,15, self)
if self:hasRoomFor(r) then
placed = true
r:paint()
table.insert(self.rooms,r)
elseif timeout <= 0 then
placed = true
end
end
end
end
Now the random
method just knows not to paint:
function Room:random(spaceX, spaceY, minSize, maxSize, runner)
local w = math.random(minSize,maxSize)
local h = math.random(minSize,maxSize)
local x = math.random(1,spaceX-w)
local y = math.random(1,spaceY-h)
return Room(x,y,w,h,runner, false)
end
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
The room layout looks good now:
Let’s do corridors now:
function GameRunner:createRandomRooms(count)
self.rooms = {}
while count > 0 do
count = count -1
local timeout = 100
local placed = false
while not placed do
timeout = timeout - 1
local r = Room:random(60,60, 4,15, self)
if self:hasRoomFor(r) then
placed = true
r:paint()
table.insert(self.rooms,r)
elseif timeout <= 0 then
placed = true
end
end
end
self:connectRooms()
end
function GameRunner:connectRooms()
for i,r in ipairs(self.rooms) do
if i > 1 then
r:connect(self.rooms[i-1])
end
end
end
That looks interesting indeed. Here’s what we get with 20 rooms in our 60x60 space:
That’s pretty highly connected but probably OK if you can’t see the whole map. Just for fun let’s try a dozen rooms.
That last one is particularly nice, I think, with that long lonely hallway down and across.
This is good stuff. Let’s commit: random connected rooms.
That’s all the learning I can stand …
So let’s sum up and I’ll take a break. I really didn’t do anything remotely like TDD here, but I did throw in a random room creation thing that allowed me to instrument things. But the problem was that I was operating in 0-based indexing mode and Lua operates in 1-indexing. So my tests, checks, asserts, and code were all off by one. Until finally my mind reset and then everything was quickly OK.
The code needs some improvement, and I think that had I been able to think of micro tests for it, it would have likely been driven out in smaller pieces. But we can do that later, or leave it for now.
We need some generalization I think, but we’ll see what we see.
So far, this is going nicely. Some of it, sure, is due to having started once and then started over. But I think the real advantage is that the tile orientation is much better than the geometric orientation with tiles imposed that I tried the first time.
See you next time!