Today we’ll talk about why no one should do what I’ve been doing the past few days, and intend to do for a few more.

One my many betters, Kent Beck, once said that all software methods are based on fear. He had in mind whatever the method’s creator most feared, the things that had caused trouble in past efforts. I believe there’s a lot of truth to that notion.

If I were to enter the business of producing software ever again, my fear, based on my past, would be the dread of having the project cancelled just before we got the thing ready to ship. It’s quite easy to avoid that possibility. You just have to

Always have the best possible product ready to ship.

Note the “just” above, one of those words that often precedes something impossible or deadly. “We just have to jump this gap.” “We just have to invent anti-gravity.” “We just have to pass this guy here on the final turn.”

Fortunately, it isn’t that difficult to have the best possible product ready to ship. What you do is, in the first few days, implement the most important capability of the product. In the next few days, implement the most important remaining capability. In the next few days …

There is an issue, of course, and that issue is software design. I was taught, and many if not most of us were taught, that you have to first design the software, and only then implement it. (We were also taught that after that, you should test it.)

This teaching turns out not to be ideal. It has been well known since the beginning of software time that your initial design cannot sustain the actual needs of the development. Relatedly, one of Adam’s first observations after leaving the Garden was “Well, that plan didn’t go well …”1

No matter how much time we spend on our initial design, and on building up our infrastructure, which goes right along with the big design first notion, the design and infrastructure won’t be quite what we need. This is especially true when we’re building something that has never been built before.

Enter iterative and incremental development.

I’ll not quibble over where those ideas came from, but for me, they first became real in 1996, on the first Extreme Programming project.

On that project, we picked an underlying design, a “production line” model, in one day. We began implementing real User Stories, product capabilities that our Customer wanted, immediately. As we added capabilities, we added the necessary infrastructure. As soon as the code’s design or clarity started to deteriorate, we refactored to improve it.

Now, I freely grant that we didn’t just magically start doing that. We had to learn a lot, we made mistakes both technical and political, and so on. But what we did do, rather nicely, was grow our product from a beginning of zero code, to thousands of classes and tens of thousands of methods, with the aid of tens of thousands of tests, over a period of years, and never got stuck.

That’s how I’d want my software to be written if I were to do a software product ever again. See How to Impose Agile for thoughts on what I’d do today.

But That’s Exactly What We’re Not Doing Here

I don’t have a product here. I’m just building hex things, more or less to see what happens. I might use some of the ideas in the Dung game, or I might not. I might do some new game, or I might not.

Arguably, I’m just messing around, which is always OK if no one is paying you for it.

I could claim that my product is a library of useful routines for managing hex tiles for games.

I could claim that, but my advice for how to build a library is to build a thing that uses the library, and then extract the stuff that looks like the library. Even in building a library, I’d recommend building a product on top of what would become the library, because it’s only in building the product that we find out what the library should actually be like.

And I’m not doing that, either.

So, and I do mean this seriously, I do not recommend that you start with lots of design or lots of infrastructure if you are trying to build a product on top of that design and infrastructure. I think you’ll do better to build features, and to iteratively and incrementally drive out the design, refactoring all the time to keep things well-designed and spiffy.

And I’m not doing that. This is not a product. This is an exploration of hexes, and an exploration of TDDing graphical things, and an activity that keeps me off the streets.

Don’t be like me. But do follow along … I think it’s interesting and hope that you do as well. Let’s get to it.

What Now?

Well, it appears that we have the ability to define hexes, using cubical coordinates, which are really weird, and we even have the coordinates ensuring that they meet the rules for cubical hex coordinates, that `x + y = z == 0’. Nice.

There is a related kind of hex coordinates, called “axial”, that just uses two coordinates, q and r, since the third coordinate of a legal hex is implied by the invariant requiring the sum to be zero. Basically 3rd = - 1st - 2nd. Our code currently uses an axial computation here:

-- algorithm from Red Blob https://www.redblobgames.com/grids/hexagons/
-- adjusted for our coords
--local cos30 = 0.8660254038 = sqrt(3)/2
--local sin30 = 0.5
--local sqrt(3) = 1.7320508076

function Coord:screenPos()
    local q = self.x
    local r = self.z
    local x = (1.7320508076*q + 0.8660254038*r) -- sqrt(3), sqrt(3)/2
    local y = -1.5*r -- we invert z axis for z upward
    return vec2(x,y)
end

Of course we could just substitute in our x and z here, but I left it like this to better match the Red Blob version, since we’re cribbing off his paper.

I am tempted right now to build a way to create a Coord in the axial q,r style, and to get an axial coordinate back out.

This is exactly why we shouldn’t start with infrastructure, and why we should build our library by creating an app to use it. I have no reason to want axial coordinates. It just seems like it would fill in a gap in the idea of this library.

It’s waste. If we need it, we can do it any time. If we don’t need it, doing it is wasted. So I’m not going to do it, but I can tell you it’d be so easy I just want to … No! Bad Ron! No!

Here’s something we might actually want: a big patch of hexes. I don’t want to worry yet about building a recrangular batch. But a hexagonal batch of hexes would look nice on the screen, and that, at least, looks a bit like a game-play area.

Honeycomb

We’ll call the idea Honeycomb, just because. Now I was wondering a day or so ago how to draw a big batch of hexes, and didn’t have a great idea. I was thinking something like start somewhere, draw all the ones that surround you, then for each of those draw all the ones that surround it, except don’t draw duplicates, repeat.

That would have worked. It’s kind of how you do a flood fill to find distances. But, sure enough, Red Blob has a better way. Amit shows how to draw rings.

Imagine a Hex at the center, 0-0-0. It has six directions, and if we draw a hex at each of the center one’s directions, we have a ring of hexes of radius one. So … if we knew how to draw rings of arbitrary radius, we could draw a 2-ring, a 3-ring, all the way out to whatever size honeycomb we wanted.

There’s an interesting pattern to the rings:

rings

If we do it right, in the ring of radius N, we draw N hexes in a given direction, then N in the next direction, repeat 6 times. Each side of a radius N ring has N+1 hexes on a side, and the corner ones are shared between two sides.

There’s a sample algorithm on the Red Blob page and I fully intend to borrow it. But I would like to test it. I would even like to test-drive it if I can figure out how.

I think I’ll start naively, by typing in all the coordinates literally and checking them. I get this far:

        _:test("Ring 1", function()
            local coords = { {1,-1,0}, {1,0,-1}, {0,1,-1}, {-1,1,0}, {-1,0,1}, {0,-1,1} }
            
        end)

Then I realize that I have no structure for holding my hexes. My HexArray thing from before isn’t appropriate. now, I could just stuff them into a simple array one after another, but it seems fair to think that given a Coord, I’d like to be able to find the game’s Hex that had that coordinate. So I think we want a table indexed by Coord, pointing to a Hex. Which means that Coord needs to be able to hash.

I think Coords will work as table indexes, because they do have a metamethod for equality, which we wrote when testing them:

function Coord:__eq(aCoord)
    return aCoord:is_a(Coord) and self.x == aCoord.x and self.y == aCoord.y and self.z == aCoord.z
end

I think we’d better test that first.

        _:test("Coord hashing works", function()
            local tab = {}
            local c1 = {1,0,-1}
            local c2 = {1,1,0}
            local c3 = {1,0,-1}
            tab[c1] = "first"
            tab[c2] = "second"
            tab[c3] = "replaced"
            _:expect(tab[c2]).is("second")
            _:expect(tab[c1]).is("replaced")
            _:expect(tab[c3]).is("replaced")
        end)

The test fails. I was afraid of that. That’s why we tested it. A search to see whether Lua has a way of dealing with this shows that it hasn’t. So we need a way to create a unique key from the Coord. Let’s try a string.

        _:test("Coord hashing works", function()
            local tab = {}
            local c1 = {1,0,-1}
            local c2 = {1,1,0}
            local c3 = {1,0,-1}
            tab[c1.key] = "first"
            tab[c2.key] = "second"
            tab[c3.key] = "replaced"
            _:expect(tab[c2.key]).is("second")
            _:expect(tab[c1.key]).is("replaced")
            _:expect(tab[c3.key]).is("replaced")
        end)

I’ve decided to compute the key when I create the Coord. I very much do not like having to remember to say c1.key but I plan to deal with that elsewhere. Remind me, OK?

function Coord:init(x,y,z)
    self.x = x
    self.y = y
    self.z = z
    if self:invalid() then error("Invalid Coordinates") end
    self.key = tostring(x).."."..tostring(y).."."..tostring(z)
end

I expect this to make my test run. However, this happens:

3: Coord hashing works -- Tests:34: table index is nil

That line is this one:

            tab[c1.key] = "first"

And it seems to be saying that c1.key is nil. Test that.

        _:test("Coord hashing works", function()
            local tab = {}
            local c1 = {1,0,-1}
            _:expect(c1.key).is("1.0.-1")
...

That fails. What have I done wrong?

OK, Dumb mistake. I’m not creating Coords in my test. Whack myself upside head. Fix test.

        _:test("Coord hashing works", function()
            local tab = {}
            local c1 = Coord(1,0,-1)
            _:expect(c1.key).is("1.0.-1")
            local c2 = Coord(1,1,0)
            local c3 = Coord(1,0,-1)
            tab[c1.key] = "first"
            tab[c2.key] = "second"
            tab[c3.key] = "replaced"
            _:expect(tab[c2.key]).is("second")
            _:expect(tab[c1.key]).is("replaced")
            _:expect(tab[c3.key]).is("replaced")
        end)

Nice failure now:

3: Coord hashing works -- Tests:117: Invalid Coordinates 1,1,0

Sure enough, they don’t add to zero. I am on a roll here, and not a good one. We’ll make this work and then I’ll have a short break.

        _:test("Coord hashing works", function()
            local tab = {}
            local c1 = Coord(1,0,-1)
            _:expect(c1.key).is("1.0.-1")
            local c2 = Coord(1,-1,0)
            local c3 = Coord(1,0,-1)
            tab[c1.key] = "first"
            tab[c2.key] = "second"
            tab[c3.key] = "replaced"
            _:expect(tab[c2.key]).is("second")
            _:expect(tab[c1.key]).is("replaced")
            _:expect(tab[c3.key]).is("replaced")
        end)

function Coord:init(x,y,z)
    self.x = x
    self.y = y
    self.z = z
    if self:invalid() then error("Invalid Coordinates "..x..","..y..","..z) end
    self.key = x.."."..y.."."..z
end

That’s close enough to commit: Coords have unique key to hash upon.

OK. When stupid, take a break. I’ll be back in a few.

A Few Units of Time Have Elapsed

So, what happened there? I was researching whether Lua has a way of defining a hash function on tables, which I could have used to index my upcoming hex collection by coordinate. So I was thinking “tables”. So I coded tables. And tables weren’t what I wanted.

So I took a break, which I should probably do more often when I get in trouble. And, after a while of just breaking, I was thinking about what it is that I probably want for these hexes and collections thereof.

Aye, there’s the rub. I’m not building anything with these babies, so I have no understanding of what I might need. Don’t be like Ron. Build things with your tools, don’t just blindly make tools.

OK, let’s see if we can make up a tiny sort of “app” for our hexes, real soon now.

When you looked at the Red Blob page, did you notice that you can touch his examples and they animate? Did you notice that sometimes, touching the example changes, not only the picture, but the text describing the picture? That stuff is great!

Maybe I’ll try something a tiny bit like that. But not yet. For now, I think I can get close enough to something I want.

Here’s what I’ll target as something larger than a few functions.

Draw a large honeycomb of hexes, some number of rings, and make one of the inner rings be tinted a light background color.

That’s going to give us some fun, including the rather interesting issue of how one fills a hexagon. So that’s where we’re going. We’re not ready yet, but now we have an excuse to build a collection of hexes.

I’m going to call a collection of hexes a Comb, short for honeycomb. I was going to call it a Hive, but a Hive has more than one Comb, so maybe there’ll be a use for that word later. Anyway, it’s just a word. What I need is a test for a Comb, and then I can get back to my rings.

        _:test("Hive", function()
            local comb = Comb()
            local h1,h2,h3
            h1 = Hex(1,-1,0)
            comb:add(h1)
            h2 = Hex(-1,0,1)
            comb:add(h2)
            h3 = Hex(1,1,-2)
            comb:add(h3)
            local g1, g2, g3
            g1 = comb:atXYZ(1,-1,0)
            _:expect(g1).is(h1)
            g2 = comb:atCoord(Coord(-1,0,1))
            _:expect(g2).is(h2)
            g3 = comb:atQR(1,-2)
            _:expect(g3).is(h3)
        end)

Now this is a pretty big test. It posits three accessors atXYZ, atCoord, and atQr. Clearly we don’t need all three. This is the standard thing with building a library without an app. But I don’t know which I’ll want, and so I build them all. Don’t be like Ron.

But it’s fun, isn’t it?

Here’s my start at Comb:

Comb = class()

function Comb:init()
    self.tab = {}
end

function Comb:add(aHex)
    tab[aHex:key()] = aHex
end

function Comb:atXYZ(x,y,z)
    local key = Coord(x,y,z).key
    return self.tab[key]
end

I’ve decided that the Hex has a method key, and of course it’ll use its Coord’s key. Just now, key is an attribute of Coord, and that’s weird and will lead to confusion with the method of the same name. I’ll sort that out shortly, or get in trouble if I don’t.

Now I need Hex to be implemented properly, and I think all I need is the key method,

function Hex:key()
    return self.coord.key
end

I could have been, and perhaps should have been, using the test to drive out these things, but sometimes one just gets rolling. Let’s test now. I expect the XYZ one to work.

4: Hive -- Tests:198: attempt to index a nil value (global 'tab')

See, that’s why I should have run the tests sooner. And real soon now, I should break these classes out into their own tabs: right now they are all inside the Tests tab, which is how I start things growing. Anyway, we need a self.:

function Comb:add(aHex)
    self.tab[aHex:key()] = aHex
end
4: Hive -- Tests:55: attempt to call a nil value (method 'atCoord')

As expected. The atXYZ worked. Now:

function Comb:atCoord(aCoord)
    return self.tab[aCoord.key]
end

I expect the coord test to work and the QR to fail.

~~~lua4: Hive – Tests:57: attempt to call a nil value (method ‘atQR’)


I have some duplication here but let's make this test run first.

~~~lua
function Comb:atQR(q,r)
    local coord = Coord(q, -q-r, r)
    return self.tab(coord.key)
end

That’s the standard definition of QR, x = q, z = r, y to balance. I expect the test to run.

Except it doesn’t. Because I put parens in there instead of brackets.

function Comb:atQR(q,r)
    local coord = Coord(q, -q-r, r)
    return self.tab[coord.key]
end

Hive test runs. We have duplication. Refactor:

function Comb:atXYZ(x,y,z)
    local key = Coord(x,y,z).key
    return self.tab[key]
end

function Comb:atCoord(aCoord)
    return self.tab[aCoord.key]
end

function Comb:atQR(q,r)
    local coord = Coord(q, -q-r, r)
    return self.tab[coord.key]
end

Extract return self.tab[key]:

function Comb:atKey(aKey)
    return self.tab[aKey]
end

No, wait. Leave that method but first refactor this:

function Comb:atXYZ(x,y,z)
    local key = Coord(x,y,z).key
    return self:atKey(key)
end

To this:

function Comb:atXYZ(x,y,z)
    return self:atCoord(Coord(x,y,z).key)
end

And refactor this:

function Comb:atQR(q,r)
    local coord = Coord(q, -q-r, r)
    return self.tab[coord.key]
end

To this:

function Comb:atQR(q,r)
    return self:atCoord(Coord(q, -q-r, r))
end

Should have run the tests. Do it now.

4: Hive  -- Actual: nil, Expected: table: 0x281faa900

See? And which one is that? it’s the atXYZ where I left .key in there. If I’d run the test as soon as I completed that tiny refactoring that couldn’t possibly go wrong, I’d have had to do less thinking.

function Comb:atXYZ(x,y,z)
    return self:atCoord(Coord(x,y,z))
end

Tests run again. How we doin’?

function Comb:atXYZ(x,y,z)
    return self:atCoord(Coord(x,y,z))
end

function Comb:atCoord(aCoord)
    return self.tab[aCoord.key]
end

function Comb:atQR(q,r)
    return self:atCoord(Coord(q, -q-r, r))
end

function Comb:atKey(aKey)
    return self.tab[aKey]
end

The last method there, atKey is the one I extracted before I started reducing the other methods. I had in mind that we’d bring everyone down to that primitive method. I think we should do that. Let me do it and then talk about why.

function Comb:atXYZ(x,y,z)
    return self:atCoord(Coord(x,y,z))
end

function Comb:atCoord(aCoord)
    return atKey:(aCoord.key)
end

function Comb:atQR(q,r)
    return self:atCoord(Coord(q, -q-r, r))
end

function Comb:atKey(aKey)
    return self.tab[aKey]
end

Why? Because our table is properly indexed only by a Coordinate key, and this form makes it clear that we only do it that way. Which causes me to want to do this:

function Comb:add(aHex)
    self.tab[aHex:key()] = aHex
end

Should become:

function Comb:add(aHex)
    self:atKeyPut(aHex:key(), aHex)
end

function Comb:atKeyPut(aKey, aHex)
    self.tab[aKey] = aHex
end

Now there is only one way into the Comb’s table, and only one way out. That reduces the chance that I’ll mess it up.

I’m not convinced this is perfect. Let’s review the whole scheme:

function Comb:add(aHex)
    self:atKeyPut(aHex:key(), aHex)
end

function Comb:atXYZ(x,y,z)
    return self:atCoord(Coord(x,y,z))
end

function Comb:atCoord(aCoord)
    return self:atKey(aCoord.key)
end

function Comb:atQR(q,r)
    return self:atCoord(Coord(q, -q-r, r))
end

function Comb:atKey(aKey)
    return self.tab[aKey]
end

function Comb:atKeyPut(aKey, aHex)
    self.tab[aKey] = aHex
end

I still don’t entirely like this. In particular, I don’t like the fact that all the at methods go through Coord and thence to key, but the put method goes direct to key, not passing explicitly through Coord. And this is caused … why?

I assert that it’s caused because I decided, based partly on my test but mostly on my imagination, that I wanted access to a Comb via XYZ, QR, and Coord. That created complexity that I can’t quite eliminate, in large part because I can’t control how Lua hashes table entries. And, maybe … because I have a separate Coord object from the Hex.

Our tests are green. Let’s commit and then see if we like it better with Coord removed. Commit: Comb added.

No. The coordinates of a Hex are not the Hex. They deserve to be separate. We’ll leave things as they are for now, move on toward our rings, and see what happens. We can always refactor if we learn something new.

To create a ring, we pick a starting Hex, and then we move some number of Hexes in a given direction, then the same number in the next direction, then the same number in the next, repeat six times and Voila! you have a ring.

So, Direction. We need to have a Direction. It’s basically just data. There are six of them. A Direction is a “unit vector”, Or, for our purposes, a unit Coord, which we will add to other Coords to get new Coords, leading to new Hexes.

I want a test for that. Remember my starting test for Rings, that I deferred until we had the Comb? It looks like this:

        _:test("Ring 1", function()
            local coords = { {1,-1,0}, {1,0,-1}, {0,1,-1}, {-1,1,0}, {-1,0,1}, {0,-1,1} }
            
        end)

Those lower-case coords are the xyz triples that you’d use to move from the 0-0-0 Hex to adjacent ones. They’re arranged, because I did it on purpose, to lead to the Hex to the right of 0-0-0, and then on around the circle, counter-clockwise, angles 0,60,120, etc. Those could be coordinates, and they could be called the Directions.

Let’s use them here in the test. It’ll be a bit of a thing to get them created as globally accessible. Here’s my initial test:

        _:test("Ring 1", function()
            local comb = Comb()
            local directions = { Coord(1,-1,0), Coord(1,0,-1), Coord(0,1,-1),Coord(-1,1,0), Coord(-1,0,1), Coord(0,-1,1) }
            local prev = Coord(0,0,0) + directions[3]
            for i = 0,5 do
                local next = prev + directions[i]
                comb:add(Hex(next))
                prev = next
            end
            for i,d in directions do
                local h = comb:atCoord(d)
                _:expect(h).isnt(nil)
            end
        end)

A bit of explanation. Since my directions go east, northeast, northwest, and so on. To draw a ring around zero, I have to start in the southwest corner, which (I think) is #3 in the table. Then I just do the corresponding move once, which is the trick for a ring of radius one.

And I’ve posited that Coords can add to each other. They can’t resulting in this message:

5: Ring 1 -- Tests:64: attempt to perform arithmetic on a table value

Easily implemented. I think this is how you do it:

function Coord:__add(aCoord)
    if aCoord:is_a(Coord) then
        return Coord(self.x+aCoord.x, self.y+aCoord.y, self.z+aCoord.z)
    else
        error("Attempt to add a Coord and something else")
    end
end

Let’s see what happens.

5: Ring 1 -- Tests:147: attempt to index a nil value (local 'aCoord')

That’ll the if line, calling is_a. The message is saying that aCoord is nil.

Dammit. Tables start at 1, not zero.

Much Later …

I’ve been heads-down trying to make this work. I finally realized that I can’t create a Hex from a Coord, so the loop needed to be changed as shown here:

            for i = 1,6 do
                local next = prev + directions[i]
                local hex = Hex(next.x, next.y, next.z)
                comb:add(hex)
                prev = next
            end

I also instrumented the test with error messages, thus:

            for i,d in ipairs(directions) do
                local h = comb:atCoord(d)
                local err = "at "..tostring(d).." found "..tostring(h)
                _:expect(h,err).isnt(nil)
            end

With this result:

5: Ring 1 at Coord(1,-1,0) found nil -- Actual: nil, Expected: nil
5: Ring 1 at Coord(1,0,-1) found Hex(Coord(1,0,-1)) -- OK
5: Ring 1 at Coord(0,1,-1) found Hex(Coord(0,1,-1)) -- OK
5: Ring 1 at Coord(-1,1,0) found nil -- Actual: nil, Expected: nil
5: Ring 1 at Coord(-1,0,1) found nil -- Actual: nil, Expected: nil
5: Ring 1 at Coord(0,-1,1) found nil -- Actual: nil, Expected: nil

Now, of course, I want to know what cells I do have in there. Surely there are six, unless I’ve gone the wrong way or something. Let’s just rip the guts out of the comb and print the keys:

            local s = "keys: "
            for k,v in pairs(comb.tab) do
                s = s.."   "..k
            end
            print(s)

The result:

keys:    1,0,-1   2,1,-3   0,1,-1   1,2,-3   0,2,-2   2,0,-2

It wandered off. Makes me wonder whether my direction addition isn’t right, or whether my directions aren’t in the right order. If both are correct, and I’m going around the ring, all the Coords should be the unit ones shown here:

coords

Let’s first see whether the directions are in the right order. The intention, if starting the ring at 3 is to work, is that the directions should be east, northeast, northwest, west, southwest, southeast.

Looking at the picture, I want east: (1,-1,0), northeast:(1,0,-1), northwest:(0,1,-1), west:(-1,1,0), southwest:(-1,0,1), southeast(0,-1,1)

That sure seems to be what I have. I honestly don’t know what’s wrong. If I were wise, I’d revert and do this over. I am not wise, so I’m going to try to debug it,

Printing out the prevs and nexts says that I’m starting at 0,1,-1, which isn’t the southwest point I wanted to start at. When I manage to work out that southwest is 5, the following test passes:

        _:test("Ring 1", function()
            local comb = Comb()
            local directions = { Coord(1,-1,0), Coord(1,0,-1), Coord(0,1,-1),Coord(-1,1,0), Coord(-1,0,1), Coord(0,-1,1) }
            local prev = Coord(0,0,0) + directions[5]
            for i = 1,6 do
                local next = prev + directions[i]
                local hex = Hex(next.x, next.y, next.z)
                comb:add(hex)
                prev = next
            end
            for i,d in ipairs(directions) do
                local h = comb:atCoord(d)
                local err = "at "..tostring(d).." found "..tostring(h)
                _:expect(h,err).isnt(nil)
            end
            local s = "keys: "
            for k,v in pairs(comb.tab) do
                s = s.."   "..k
            end
            print(s)
        end)

Commit: Ring 1 test runs. I refuse to tell you how long I’ve dithered on this. Now let’s pull out a function to make a ring, and then use it to display our ring. I think the rule will be to put a hex in the middle. Our function will be called “HexDisc”, and will ultimately take a parameter to make it draw the big picture we want.

function HexDisc(ignoredCount)
    local comb = Comb()
    comb:add(Hex(0,0,0)
    local directions = { Coord(1,-1,0), Coord(1,0,-1), Coord(0,1,-1),Coord(-1,1,0), Coord(-1,0,1), Coord(0,-1,1) }
    local prev = Coord(0,0,0) + directions[5]
    for i = 1,6 do
        local next = prev + directions[i]
        local hex = Hex(next.x, next.y, next.z)
        comb:add(hex)
        prev = next
    end
    return comb
end

Now to use it in our test:

        _:test("Ring 1", function()
            local directions = { Coord(1,-1,0), Coord(1,0,-1), Coord(0,1,-1),Coord(-1,1,0), Coord(-1,0,1), Coord(0,-1,1) }
            local comb = HexDisc(1)
            for i,d in ipairs(directions) do
                local h = comb:atCoord(d)
                local err = "at "..tostring(d).." found "..tostring(h)
                _:expect(h,err).isnt(nil)
            end
            local s = "keys: "
            for k,v in pairs(comb.tab) do
                s = s.."   "..k
            end
            print(s)
        end)

I had to duplicate directions. We’ll fix that in post.

Test runs. Now let’s change the Main to display our disc.


    local comb = HexDisc(1)
    for k,hex in pairs(comb.tab) do
        pushMatrix()
        local coord = hex:screenPos()
        local cx = coord.x
        local cy = coord.y
        translate(cx,cy)
        for rot = 0,5 do
            local bl = hex:boundaryLine(rot)
            line(table.unpack(bl))
        end
        popMatrix()
    end

disc

That’s what I’m talkin’ about!

Let’s sum up and get the heck out of here.

Summary

I think I’d have to say that I wasn’t the sharpest tool in the shed today, though I feel perfectly OK. But the record stands for itself. I was stalled, a number of times, by very simple mistakes. The one that held me up the most was that I tried to create a Hex by passing a Coord into it, and that gave me a truly interesting error, because it was exercising my new Coord __add capability.

The second worst was that I wasn’t starting my ring in the correct southwest location, so it wandered off and created some other odd shape.

I’m curious what that shape is. Let’s find out. It was one of these two:

offset side

offset up

I could look back to see what the incorrect starting script was, but it was wrong twice. I think I had each of those results at some point.

This does speak for displaying graphical results like this, doesn’t it? It’s really hard to write a test that checks to see that we have the right hexes. Or at least hard for me. And it’s easy to see when things aren’t right.

Must think about that.

But the evil of the day is sufficient thereto, and for now, we have a new feature (a very small playing field), and everything is rather solidly tested and fairly well test-driven.

Enough. See you next time!


  1. Personal communication.