Dungeon 228--Don't Do This!
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:
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:
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
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:
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!
-
Personal communication. ↩