I can’t face reality, so I’ll keep working on this little imaginary world. At this moment, no idea quite what I’ll work on.

0715, awake, left hanging between Purgatory and Hell. Guess I’ll try to quiet my mind with some code. Maybe you can quiet yours by reading this and enjoying it, as something to learn from or something to laugh at.

When last we left our heroes, they were waiting for the dungeon to be built. We were “nearly done” with separating the rooms, which are created all randomly in a blob. We have a function that will move the rooms one step apart, using a simple separation behavior lifted from the ancient Boids project.

I’ll start by wiring that into the program to provide a nice animation of rooms forming and swarming.

Here’s the stuff that does the work:

-- class method

function Room:adjustAll(rooms)
    for i,room in ipairs(rooms) do
        room:moveAwayFrom(rooms)
    end
end

-- instance methods

function Room:moveAwayFrom(rooms)
    local v = self:combinedImpactOf(rooms)
    local pos = self:center() + v
    self.x = pos.x
    self.y = pos.y
end

function Room:combinedImpactOf(allRooms)
    local rooms = self:intersectors(allRooms)
    if #rooms == 0 then return vec2(0,0) end
    local imp = vec2(0,0)
    for i,r in ipairs(rooms) do
        imp = imp + self:impactOf(r)
    end
    return imp/#rooms
end


function Room:impactOf(aRoom)
    local d = self:dist(aRoom)
    if d == 0 then return vec2(0,0) end
    return (self:center()-aRoom:center()):normalize()
    -- used to divide by dist to adjust impact
end

function Room:intersectors(rooms)
    local int = {}
    for i,r in ipairs(rooms) do
        if self ~= r and self:intersects(r) then
            table.insert(int,r)
        end
    end
    return int
end

We need to apply that adjustAll function repeatedly until it converges, which means that no room has moved. At the moment, we don’t know whether a room has moved or not. I think I’d like Room:adjustAll to return how many rooms were moved. Let’s write a test for that. I have a partial test in place with no guts that might serve:

        _:test("move all", function()
            local r1 = Room(100,100,5,5)
            local r2 = Room(101,101,5,5)
            local r3 = Room(99,99,5,5)
            local r4 = Room(106,106,5,5)
            local r5 = Room(99,101,5,5)
            local all = {r1,r2,r3,r4,r5}
            Room:adjustAll(all)
        end)

I’m not even sure how many of those overlap. I think al but one. r4, but I’d have to think to be sure. We’ll extend the test:

        _:test("move all", function()
            local r1 = Room(100,100,5,5)
            local r2 = Room(101,101,5,5)
            local r3 = Room(99,99,5,5)
            local r4 = Room(106,106,5,5)
            local r5 = Room(99,101,5,5)
            local all = {r1,r2,r3,r4,r5}
            local moved = Room:adjustAll(all)
            _:expect(moved).is(4)
        end)

And to code … this seems clear enough …

function Room:adjustAll(rooms)
    count = 0
    for i,room in ipairs(rooms) do
        if room:moveAwayFrom(rooms) then
            count = count + 1
        end
    end
    return count
end

Now here’s where the moving takes place:

function Room:moveAwayFrom(rooms)
    local v = self:combinedImpactOf(rooms)
    local pos = self:center() + v
    self.x = pos.x
    self.y = pos.y
end

So if we want to return true when v isn’t the zero vector. How about this:

function Room:moveAwayFrom(rooms)
    local v = self:combinedImpactOf(rooms)
    if v == vec2(0,0) then return false end
    local pos = self:center() + v
    self.x = pos.x
    self.y = pos.y
    return true
end

I think that should make our test run. However, it gets 5. Now I have to think.

7: move all  -- Actual: 5, Expected: 4

OK, what are those rectangles?

I’m not sure whether the one that ends at 106 is supposed to overlap the one that starts there: I thought not. Let’s add an assertion:

            _:expect(r2:intersects(r4)).is(false)

That fails, they do intersect. Should they? Here I go again with my uncertainty about intersecting. Certainly they do contain part of the same edge. And here’s intersects:

function Room:intersects(aRoom)
    return intersectCorners(self:corners(), aRoom:corners())
end

function Room:corners()
    local hw = self:halfWidth()
    local hh = self:halfHeight()
    return {self.x - hw, self.y - hh, self.x + hw, self.y + hh}
end

function intersectCorners(corners1, corners2)
    x1lo, y1lo, x1hi, y1hi = unpack(corners1)
    x2lo, y2lo, x2hi, y2hi = unpack(corners2)
    if y1lo > y2hi or y2lo > y1hi then return false end
    if x1lo > x2hi or x2lo > x1hi then return false end
    return true
end

So it’s false iff the low is strictly greater than the high or the high is strictly lower than the low. They do intersect. So let’s change r4 and get a 4 back from the count.

        _:test("move all", function()
            local r1 = Room(100,100,5,5)
            local r2 = Room(101,101,5,5)
            local r3 = Room(99,99,5,5)
            local r4 = Room(107,107,5,5)
            local r5 = Room(99,101,5,5)
            _:expect(r2:intersects(r4)).is(false)
            local all = {r1,r2,r3,r4,r5}
            local moved = Room:adjustAll(all)
            _:expect(moved).is(4)
        end)

That runs. Now we can iterate our adjustAll until everyone stops. Is it possible that they never do? I don’t think so, but we’ll put in an emergency check, perhaps. Now I want this to animate slowly in the draw cycle, just for fun. So …

function setup()
    if CodeaUnit then
        codeaTestsVisible(true)
        runCodeaUnitTests()
    end
    Rooms = createRooms(40)
    AdjustTime = ElapsedTime
    AdjustDelay = 0.05
    AdjustCount = 1
end

function draw()
    if CodeaUnit then showCodeaUnitTests() end
    pushMatrix()
    pushStyle()
    rectMode(CENTER)
    for i,r in ipairs(Rooms) do
        r:draw()
    end
    if AdjustCount > 0 then
        if ElapsedTime - AdjustTime > AdjustDelay then
            AdjustTime = ElapsedTime
            AdjustCount = Room:adjustAll(Rooms)
        end
    end
    popStyle()
    popMatrix()
end

Sweet. Here’s a video:

adjusting

For fun, I’m going to set the delay to zero and see what it looks like.

Here’s a video of a few runs at zero delay, 50 rooms:

fifty

So that’s fun.

It’s 0750. I’ve not freshened up yet but I am minded to go get a chai anyway. Commit: rooms separate correctly.

See you in a few …

0822, back with Venti Iced Chai Latte. Nom. What’s the next step for this Dung[eon]? At the game level, the next things include:

  • Carving hallways between the rooms, ensuring that the maze is connected, and ideally including a few loops to make it interesting;
  • Deciding which rooms are “interesting” and populating them with treasures, traps, and various dungeon appurtenances.
  • Doors. There probably should be doors between at least some of the rooms or hallways. Perhaps “interesting” doors.
  • Drawing the game and player and such–this can go on forever.

When we look at a dungeon maze like this one:

maze

We see that many, perhaps most of the rooms are adjacent and can support paths between them. Hey, that makes me think we should give these rooms numbers, so we can talk about them. Add that to the list … and do it.

We currently create rooms this way:

...
    Rooms = createRooms(50)
...

function createRooms(n)
    local r = {}
    for i = 1,n do
        table.insert(r,Room())
    end
    return r
end

That should be a class factory method, now that we have a Room class, so I’ll move it:

function Room:createRooms(n)
    local r = {}
    for i = 1,n do
        table.insert(r,Room(i))
    end
    return r
end

-- instance methods

function Room:init(roomNumber, x,y,w,h)
    self.number = roomNumber or 666
    self.x = x or math.random(WIDTH//4,3*WIDTH//4)
    self.y = y or math.random(HEIGHT//4,3*HEIGHT//4)
    self.w = w or math.random(WIDTH//20,WIDTH//10)
    self.h = h or math.random(HEIGHT//20,WIDTH//10)
end

Now I’m going to have to change all my tests to include a room number. No, let’s do that differently, just to see if we like it:

function Room:createRooms(n)
    local r = {}
    for i = 1,n do
        table.insert(r,Room:withNumber(i))
    end
    return r
end

function Room:withNumber(n)
    local r = Room()
    r.number = n
    return r
end

-- instance methods

function Room:init(x,y,w,h)
    self.number = 666
    self.x = x or math.random(WIDTH//4,3*WIDTH//4)
    self.y = y or math.random(HEIGHT//4,3*HEIGHT//4)
    self.w = w or math.random(WIDTH//20,WIDTH//10)
    self.h = h or math.random(HEIGHT//20,WIDTH//10)
end

We make a new factory method, withNumber, where we jam in the number. I don’t like this entirely, because I was taught that one should use the Complete Creation Method pattern, where an object is created all set up. In our case here, it’s not quite all set up. I don’t expect any trouble.

Now let’s modify draw:

function Room:draw()
    pushMatrix()
    fill(255)
    stroke(255)
    strokeWidth(0)
    textMode(CENTER)
    translate(self.x,self.y)
    local nr = string.format("%d", self.number)
    text(nr,0,0)
    fill(0,255,0,25)
    stroke(0,255,0)
    strokeWidth(2)
    rect(0,0, self.w, self.h)
    popMatrix()
end

Uh oh, a test failed.

1: create 25 rooms -- Tests:17: attempt to call a nil value (global 'createRooms')

Oops. The fix:

        _:test("create 25 rooms", function()
            local rooms = Room:createRooms(25)
            _:expect(#rooms).is(25)
        end)

Not a very interesting test but it was worth having to get us started. We’ll keep it as an historical artifact.

Now we have this result:

numbered

In that picture, most of the rooms are able to be connected by a door between them. But some cannot be, such as

  • #47 at 10 o’clock
  • #16 at 2 o’clock
  • #23 just below 3 o’clock
  • #7 at 5 o’clock

We don’t mind if a room is only connected once. But there’s not much point to a room that isn’t connected at all. Oh, we could have some magical teleport way of getting in and out, saying XYZZY or something, but really we want all our rooms connected at least by hallways. So we’ll need to see how to carve hallways through the bedrock to get to these rooms.

There are other perhaps interesting questions. Take a look at 2,26,14,48 at 11 o’clock. We could connect them in a ring, or we could skip some of the doors. Now the official cool way to deal with this is to compute a minimal path tree through all the rooms. Then, if you’ve a mind to, you tweak that path to add a few loops to make it more interesting (and more accessible).

There are a few ways to do this. A naive one is described here. I call that naive because it randomly places rooms until it finds a place for them that doesn’t overlap, repeat until whenever.

Then that example just draws corridors between the nth room and the n+1st room, without regard to how far apart they are. That’s going to get weird, it seems to me, and it seems likely to generate a lot of long walking paths to no purpose. But it’s interesting. In our case, could we reasonably do that? I think we’d have to leave more space between rooms, if nothing else.

That article includes a link to the PCG Wiki, which is simply full of interesting articles, such as this one on dungeon generation. There’s also roguebasin, again with masses of possibly useful material.

Creation of a good playable game is probably beyond the scope of these articles. I’m just doing this because of the Zoom Ensemble who are, well, ensembling on one of the members’ little rogue game development. So I’ll probably stop well before we have a game. Unless I don’t. This could be a new career for me.

The roguebasic wiki is massive. You could read there for ages, and you might even find something you wanted.

There are some really interesting and technical algorithms out there, a thing called “Prim” and a triangulation algorithm from Delaunay, which is useful for generating a minimal tree of paths, if I understand it.

We could pick one of those and go off and do it. Instead, let’s wander off into the dungeon on our own and see what we can come up with.

Looking at the pictures, it seems that the dungeon may be “mostly connected”, by which I mean that most of the rooms, or at least big chunks of rooms, are adjacent enough to allow for doors between them to connect them into a coherent whole. So what if we were to start at some room, and find all the rooms that are adjacent–for a suitable meaning of adjacent–and colored them all some color. Then we’d look for rooms that weren’t colored, and repeat. What would happen?

I’m of a mind to find out.

FAIR WARNING: Everything from here on down is rife with mistakes. Unless you enjoy seeing me going down a rathole, just skim or stop here.

So back to our picture:

numbered

Starting in the top left quadrant, it looks to me as if we’d color in quite a large blob, but we’d miss out #21, right about 12 o’clock, because its adjacency with #9 isn’t enough to support a door. We’ll have to decide how much is enough, but that doesn’t look like a good spot for a door to me.

Look along the 9 o’clock axis at 43 and 15. They’re a bit further apart, not quite touching, but we could go either way on something so close as that. I don’t know, let’s just try it.

How can we define adjacency? Two cells are adjacent if corresponding walls (East to West, North to South, etc.) are no further than distance D apart, and the intersection of their span along those walls is at least width W. (I may have to draw a picture for you to understand that. Or for me. We’ll see.)

For a given pair of rooms, room and potential neighbor, we have to consider four pairs of line segments, N:S, E:W, S:N, W:E to see if they meet adjacency criteria.

We can TDD this. Therefore, let’s do. How about this:

        _:test("room adjacency", function()
            local r1 = Room(500,500, 100,100)
            checkCorners(r1, 450,450,550,550)
            local r2 = Room(601, 601, 100, 100)
            checkCorners(r2, 551,551,651,651)
            _:expect(r1:ewAdjacent(r2)).is(true)
        end)

In this test, r1’s east wall is within 1 of r2’s west wall, and the overlap is 50ish, so I expect to find them adjacent. We don’t have the method yet, so the test fails.

I’ll show my work in a moment, but I’ve convinced myself that these two rooms do not overlap because r2 is a full 100 meters higher than r1. Trying 50.

Here’s what I wrote. I’m not at all sure it’s correct:

function Room:ewAdjacent(aRoom)
    if aRoom.x - self.x < 1 then return false end
    if self:north()-aRoom:south() < self:overlap() then return false end
    if aRoom:north()-self:north() < self:overlap() then return false end
    return true
end

function Room:overlap()
    return 30
end

function Room:north()
    return self:corners()[4]
end

function Room:south()
    return self:corners()[2]
end

Let me put in a couple of rooms that should fail to be EW adjacent and see if this works.

Hm we got this:

8: room adjacency -- Room:141: attempt to index a nil value (local 'aRoom')

That doesn’t bode well for my code at all. Oh, the test was bad.

        _:test("room adjacency", function()
            local r1 = Room(500,500, 100,100)
            checkCorners(r1, 450,450,550,550)
            local r2 = Room(601, 550, 100, 100)
            checkCorners(r2, 551,500,651,600)
            _:expect(r1:ewAdjacent(r2)).is(true)
            local r3 = Room(601, 580, 100, 100)
            _:expect(r1:ewAdjacent(r3)).is(false)
        end)

This runs. Could I possibly be correct? Let’s reason about that code in ewAdjacent and see if we can improve it as well.

function Room:ewAdjacent(aRoom)
    if aRoom.x - self.x < 1 then return false end
    if self:north()-aRoom:south() < self:overlap() then return false end
    if aRoom:north()-self:north() < self:overlap() then return false end
    return true
end

I’ve just defined north and south to return the lower y and upper y from the corners function. I could have computed them directly but that would produce an odd form of duplication where we compute the top and bottom twice. We’ll worry about the speed later, perhaps caching all those values? No, we’ll just not worry until the system is too slow. This is the way.

So that code says: …

Oh. It’s wrong and worked by accident. We need to compare east and west here, not centers.

function Room:east()
    return self:corners()[1]
end

function Room:west()
    return self:corners()[3]
end

function Room:ewAdjacent(aRoom)
    if aRoom:west() - self:east() < 1 then return false end
    if self:north()-aRoom:south() < self:overlap() then return false end
    if aRoom:north()-self:north() < self:overlap() then return false end
    return true
end

Test still works, as it should. Now, as I was saying,

First, if he’s more than 1 unit to the east of us, he’s not OK. Darn, no. This isn’t right either, is it? We can assume that we do not overlap, though maybe we shouldn’t. What do we want to be true?

If we are east-west adjacent, then his west wall must be within 1 (or whatever we pick for close enough) of our east wall. So aRoom:west - self:east should be 1 and if it’s greater he’s too far away. Test fails because I have west and east reversed. Now:

function Room:east()
    return self:corners()[3]
end

function Room:west()
    return self:corners()[1]
end

function Room:ewAdjacent(aRoom)
    if aRoom:west() - self:east() > 1 then return false end
    if self:north()-aRoom:south() < self:overlap() then return false end
    if aRoom:north()-self:north() < self:overlap() then return false end
    return true
end

So far so good. So as I was saying, if his west wall is more than 1 (or whatever) away from our east wall, he’s not adjacent. If it is, then we want to see if there’s room for a door. I’ve arbitrarily decided to use 30 points of overlap for now.

So if his south wall - our north wall is less than 30, he’s too high.And if his north wall - our ah south wall, he’s too low. Let me fix that:

function Room:ewAdjacent(aRoom)
    if aRoom:west() - self:east() > 1 then return false end
    if self:north()-aRoom:south() < self:overlap() then return false end
    if aRoom:north()-self:south() < self:overlap() then return false end
    return true
end

Are you losing confidence in this code, since I’ve changed nearly every character of it since I started? So am I, but I think a few more tests are in order. If they pass, confidence will return. If not, we’ll give up and start over on adjacency.

Or we’ll keep digging. That’s always good. We might be near the bottom of the hole.

You know what I want? I want a new room creation method that lets me type in the corners. I want then to continue to be in CENTER mode but I need to be able to create these tests without mental arithmetic. I didn’t get into computing because I was good at arithmetic. I want:

function Room:fromCorners(xLo,yLo,xHi,yHi)
    
end

And I want to test drive it. Here’s what I got and it’s a bit of a shocker:

        _:test("room from corners", function()
            local r = Room:fromCorners(450,450,550,550)
            checkCorners(r, 450,450,550,550)
            local r1 = Room:fromCorners(451,451,550,550)
            checkCorners(r1, 451,451,550,550)
        end)

This test doesn’t run:

9: room from corners xLo -- Actual: 450.5, Expected: 451

And so on. Now as it happens, when we’ve created our rooms, we’ve always given them integer starting coordinates and integer widths … even integer widths and heights in general, in our tests. The random rooms can have odd widths and heights but will still be on integer centers. We force all those values to integer:

function Room:init(x,y,w,h)
    self.number = 666
    self.x = x or math.random(WIDTH//4,3*WIDTH//4)
    self.y = y or math.random(HEIGHT//4,3*HEIGHT//4)
    self.w = w or math.random(WIDTH//20,WIDTH//10)
    self.h = h or math.random(HEIGHT//20,WIDTH//10)
end

If we do happen to get an odd width or height, then the room’s corner coordinates will come out fractional, because of this:

function Room:corners()
    local hw = self:halfWidth()
    local hh = self:halfHeight()
    return {self.x - hw, self.y - hh, self.x + hw, self.y + hh}
end

function Room:halfWidth()
    return self.w/2
end

function Room:halfHeight()
    return self.h/2
end

Long ago, one or two days ago, I decided just to let that happen. So the room I tried to create:

            local r1 = Room:fromCorners(451,451,550,550)
            checkCorners(r1, 451,451,550,550)

Cannot actually occur, because it has an odd width and as such its center will be on an integer and its corners will not. So the check needs to be fixed. And then I think I’ll be convinced that fromCorners works consistently with the rest of the system. Maybe.

        _:test("room from corners", function()
            local r = Room:fromCorners(450,450,550,550)
            checkCorners(r, 450,450,550,550)
            local r1 = Room:fromCorners(451,451,550,550) -- odd w and h
            checkCorners(r1, 450.5,450.5,549.5,549.5)
        end)

So I think that fromCorners is fine. And back to writing a test for ewAdjacent that should fail:

        _:test("room adjacency", function()
            local r1 = Room:fromCorners(450,450,550,550)
            checkCorners(r1, 450,450,550,550)
            local r2 = Room:fromCorners(551,500,651,600)
            checkCorners(r2, 551,500,651,600)
            _:expect(r1:ewAdjacent(r2)).is(true)
            local r3 = Room:fromCorners(551, 530, 651, 630)
            _:expect(r1:ewAdjacent(r3)).is(false)
        end)

Let’s draw a picture of that last test, not least to be sure it’s a fair test. I intend it to be near enough on the side, but too high up.

r1r3

Let’s do one too far down, then move on to the other sides, hopefully removing some duplication along the way.

        _:test("room adjacency", function()
            local r1 = Room:fromCorners(450,450,550,550)
            checkCorners(r1, 450,450,550,550)
            local r2 = Room:fromCorners(551,500,651,600)
            checkCorners(r2, 551,500,651,600)
            _:expect(r1:ewAdjacent(r2)).is(true)
            local r3 = Room:fromCorners(551, 530, 651, 630)
            _:expect(r1:ewAdjacent(r3)).is(false)
            local r4 = Room:fromCorners(551, 370, 651, 470)
            _:expect(r1:ewAdjacent(r4)).is(false)
        end)

Shall I draw that one too? I think yes.

r1r4

Time is fleeting. It’s 11 AM. My wife, who put in a 25 hour work day at the polls yesterday, is still asleep, so I could continue but I’m coming up on more than three hours of work, which is beyond my arbitrary limit. But let’s at least do, oh, nsAdjacency. Our north side, his south side. Some tests are needed, innit, much like the old ones.

I’m going to go for rounder numbers. Those 50s in the EW test made it harder to understand. I left them in when I converted to use fromCorners. Maybe I’ll adjust those later. Now, this should get me going:

        _:test("room NS adjacency", function()
            local r1 = Room:fromCorners(500,500,600,600)
            local r2 = Room:fromCorners(500,601,600,701) -- directly on top
            _:expect(r1:nsAdjacent(r2)).is(true)
            --[[
            local r3 = Room:fromCorners(551, 530, 651, 630)
            _:expect(r1:nsAdjacent(r3)).is(false)
            local r4 = Room:fromCorners(551, 370, 651, 470)
            _:expect(r1:nsAdjacent(r4)).is(false)
            ]]--
        end)

I copied the EW test and pasted it here. I’ve only adjusted the first room. But since I plan to copy / paste / hammer the ns from ew, let’s fill in the others to match roughly what the other test did. I think this is correct:

        _:test("room NS adjacency", function()
            local r1 = Room:fromCorners(500,500,600,600)
            local r2 = Room:fromCorners(500,601,600,701) -- directly on top
            _:expect(r1:nsAdjacent(r2)).is(true)
            local r3 = Room:fromCorners(580, 601, 680, 701)
            _:expect(r1:nsAdjacent(r3)).is(false)
            local r4 = Room:fromCorners(420, 601, 520, 701)
            _:expect(r1:nsAdjacent(r4)).is(false)
        end)

Now to drive out the new function.

9: room NS adjacency -- Tests:105: attempt to call a nil value (method 'nsAdjacent')

No surprise there. And with this:

function Room:ewAdjacent(aRoom)
    if aRoom:west() - self:east() > 1 then return false end
    if self:north()-aRoom:south() < self:overlap() then return false end
    if aRoom:north()-self:south() < self:overlap() then return false end
    return true
end

function Room:nsAdjacent(aRoom)
    if aRoom:south()-self:north() > 1 then return false end
    if self:east()-aRoom:west() < self:overlap() then return false end
    if aRoom:east()-self:west() < self:overlap() then return false end
    return true
end

The test runs. Doing this made me think of a rotation. If we rotate the ew rules, which reference west,east,north,south,north,south counter clockwise, then we would see north,south,west,east,west,east. But that’s not what I did. Is the rotation idea valid?

Is any of this working? Is this thing on? I find this stuff to be very confusing. If you don’t, I wish you were pairing with me. But I muddle on and tend to get things right in the end.

I’m going to draw another picture.

rotate

The picture convinces me that the code should say:

function Room:nsAdjacent(aRoom)
    if aRoom:south()-self:north() > 1 then return false end
    if self:west()-aRoom:east() < self:overlap() then return false end
    if aRoom:west()-self:east() < self:overlap() then return false end
    return true
end

The tests do not agree. But what failed, and why?

I’ll add some comments in my secret comment field:

        _:test("room NS adjacency", function()
            local r1 = Room:fromCorners(500,500,600,600)
            local r2 = Room:fromCorners(500,601,600,701) -- directly on top
            _:expect(r1:nsAdjacent(r2), "r2").is(true)
            local r3 = Room:fromCorners(580, 601, 680, 701)
            _:expect(r1:nsAdjacent(r3), "r3").is(false)
            local r4 = Room:fromCorners(420, 601, 520, 701)
            _:expect(r1:nsAdjacent(r4), "r4").is(false)
        end)

And see what fails:

10: room NS adjacency r2 -- Actual: false, Expected: true

But why? I have a trick in mind. Hold my beer:

function Room:nsAdjacent(aRoom)
    if aRoom:south()-self:north() > 1 then return false, "too far north" end
    if self:west()-aRoom:east() < self:overlap() then return false, "self:west-aRoom:east" end
    if aRoom:west()-self:east() < self:overlap() then return false, "aRoom:west-self:east" end
    return true, "ok"
end

I’ll return two values, the boolean that everyone wants, and a reason that my test wants:

        _:test("room NS adjacency", function()
            local r1 = Room:fromCorners(500,500,600,600)
            local r2 = Room:fromCorners(500,601,600,701) -- directly on top
            local adj,reason
            adj,reason = r1:nsAdjacent(r2)
            _:expect(adj, "r2").is(true)
            _:expect(reason).is("ok")
            local r3 = Room:fromCorners(580, 601, 680, 701)
            adj,reason = r1:nsAdjacent(r3)
            _:expect(adj, "r3").is(false)
            _:expect(reason).is("ok")
            local r4 = Room:fromCorners(420, 601, 520, 701)
            adj,reason = r1:nsAdjacent(r4)
            _:expect(adj, "r4").is(false)
            _:expect(reason).is("ok")
        end)

For now, I’ll just check for “ok” all the time, though we should see identified failures for the false cases. But with this new rotated code, this happens first:

10: room NS adjacency r2 -- Actual: false, Expected: true
10: room NS adjacency  -- Actual: self:west-aRoom:east, Expected: ok

So WTF. was the original formulation correct, and my rotation idea just wrong?

The example is:

            local r1 = Room:fromCorners(500,500,600,600)
            local r2 = Room:fromCorners(500,601,600,701)

We have 100 percent east-west overlap, 100 pixels. The code says:

function Room:nsAdjacent(aRoom)
    if aRoom:south()-self:north() > 1 then return false, "too far north" end
    if self:west()-aRoom:east() < self:overlap() then return false, "self:west-aRoom:east" end
    if aRoom:west()-self:east() < self:overlap() then return false, "aRoom:west-self:east" end
    return true, "ok"
end

We errored on self:west-aRoom:east. Are east and west wrong?

function Room:east()
    return self:corners()[3]
end

function Room:west()
    return self:corners()[1]
end

No, the corners are xSouth,yWest, xNorth,yEast, are they not?

Ah, OK. We need to be more clear which we’re checking. There are two cases, too far right, and too far left. Let me change the comments and then make the code match them. First:

function Room:nsAdjacent(aRoom)
    if aRoom:south()-self:north() > 1 then return false, "too far north" end
    if self:west()-aRoom:east() < self:overlap() then return false, "too far west" end
    if aRoom:west()-self:east() < self:overlap() then return false, "too far east" end
    return true, "ok"
end

OK if aRoom is not too far west, then his west edge is still greater than my west edge so the subtract must be reversed. And if aRoom is not too far east, then his west edge is still less than my east edge, so I think that one is reversed as well. Which leads me to this:

function Room:nsAdjacent(aRoom)
    if aRoom:south()-self:north() > 1 then return false, "too far north" end
    if aRoom:east()-self:west() < self:overlap() then return false, "too far west" end
    if self:east()-aRoom:west() < self:overlap() then return false, "too far east" end
    return true, "ok"
end

Is that essentially what I used to have? I’m not sure. Let’s test.

10: room NS adjacency  -- Actual: too far east, Expected: ok
10: room NS adjacency  -- Actual: too far west, Expected: ok

Do I agree with these results? Those are r3 and r4 respectively, should put the comments in.

10: room NS adjacency r3 -- Actual: too far east, Expected: ok
10: room NS adjacency r4 -- Actual: too far west, Expected: ok

I agree that those are the answers based on what I was testing:

        _:test("room NS adjacency", function()
            local r1 = Room:fromCorners(500,500,600,600)
            local r2 = Room:fromCorners(500,601,600,701) -- directly on top
            local adj,reason
            adj,reason = r1:nsAdjacent(r2)
            _:expect(reason,"r2").is("ok")
            local r3 = Room:fromCorners(580, 601, 680, 701)
            adj,reason = r1:nsAdjacent(r3)
            _:expect(reason,"r3").is("too far east")
            local r4 = Room:fromCorners(420, 601, 520, 701)
            adj,reason = r1:nsAdjacent(r4)
            _:expect(reason, "r4").is("too far west")
        end)

I still think there should be a rotation kind of thing to do here but I’m not going to look for it. Instead, let’s get the other two directions working. They are “sn” and “we”. Can we do those by reversing the calls to the ns and ew? You’d think so, wouldn’t you? Let’s write some tests and try it.

Here’s my proposed SN test. I just lowered the target guys so that their max Y was 499, one less than r1’s 500.

        _:test("room SN adjacency", function()
            local r1 = Room:fromCorners(500,500,600,600)
            local r2 = Room:fromCorners(500,399,600,499) -- directly under
            local adj,reason
            adj,reason = r1:snAdjacent(r2)
            _:expect(reason,"r2").is("ok")
            local r3 = Room:fromCorners(580, 399, 680, 499)
            adj,reason = r1:snAdjacent(r3)
            _:expect(reason,"r3").is("too far east")
            local r4 = Room:fromCorners(420, 399, 520, 399)
            adj,reason = r1:snAdjacent(r4)
            _:expect(reason, "r4").is("too far west")
        end)

And after only one try, I remembered to change the calls from ns to sn. Now to test, getting a fail because no sn exists.

11: room SN adjacency -- Tests:127: attempt to call a nil value (method 'snAdjacent')

As expected. Let’s try the reversal:

function Room:snAdjacent(aRoom)
    return aRoom:nsAdjacent(self)
end

I have high hopes for this test. They are dashed:

11: room SN adjacency r3 -- Actual: too far west, Expected: too far east
11: room SN adjacency r4 -- Actual: too far north, Expected: too far west

Ah … the too far east vs west is valid, because we’re upside down. If A is too far east of B then B is too far west of A. Our clever messages are too specific to do the call back. But what’s wrong with the r4 one? A typo:

            local r4 = Room:fromCorners(420, 399, 520, 399)

Should be 499 there at the end. Now the test is working, the call back does work, but we need to improve those messages. Is there any way to do that? Or should we decide that this idea has done what it needed to, given us a bit of information when we needed it, and remove them?

Remove. But that will be a bit of an edit and it’s past time to go for lunch. Back soon …

OK, not really soon, it’s 1510. Anyway, I think I’ll just create the weAdjacent without testing, so I can try the coloring idea.

function Room:weAdjacent(aRoom)
    return aRoom:ewAdjacent(self)
end

There’s probably some clever way to combine the other bits, but we’ll save that for later. Now let’s see. Only after the separation is done can we do the coloring. We can select an element from Rooms, like the first one, then search for adjacent rooms, coloring them and removing them from the table until we find no more. Then pick a new color and repeat, I suppose. This is not going to be terribly fast, is it, since we’ll have to check all the ones who are of the current color against all the others. We should really do all this at build time but I want to watch things happen for now.

Well, in for a penny. Here goes.

Much later …

That did not go well. I’m going to spare you some thrashing and revert for the next article. Suffice to say that my adjacency checks don’t work: they find things adjacent that aren’t. And I have concerns over this scheme anyway.

See you next time!

Dung-1