Already wrote one article today. Not sure about this one. Probably I’ll find an acorn somewhere.

My original plan for today was to try again to get the game to draw a hex map, leading toward actually being able to play on it. Since it’s already 1020 local, I think I’ll defer that, but make some preparations instead. Yesterday we got the old tiles structure completely out of the game. At least we think we did. Today I thought I’d look at the auxiliary functions around the map and see whether they should be improved to use the new Map objects.

In particular, I’m aware of some direction kinds of things that came up yesterday. Let’s start there.

function MapCell:neighborIndices()
    return { vec2(0,1), vec2(1,0), vec2(0,-1), vec2(-1,0) }
end

function MapCell:neighbors()
    local near = {}
    for i,nxy in ipairs(self:neighborIndices()) do
        local n = self:getLegalCell(self.x+nxy.x, self.y+nxy.y)
        if n then
            table.insert(near,n)
        end
    end
    return near
end

The neighborIndices method returns what we are now calling “directions”, one-step vectors to adjacent tiles. The ones there are the cartesian directions. There are also hex directions, of which there are six. If the PathFinder is to work on hex tiles, and there’s no reason why it shouldn’t, then that function there needs to return hex directions when we’re on a hex map.

There must also be similar logic in Dungeon, or whoever it is that does neighbor kinds of things over that way. Let’s have a look there, to decide where to start.

Oh this is interesting! Look:

function Dungeon:neighbors(tile)
    local tPos = tile:pos()
    local offsets = self:neighborOffsets()
    return map(offsets, function(offset) return self:getTile(offset + tPos) end)
end

function Dungeon:neighborOffsets()
    return { 
        vec2(-1,-1), vec2(0,-1), vec2(1,-1),
        vec2(-1,0),              vec2(1,0),
        vec2(-1,1),  vec2(0,1),  vec2(1,1)
    }
end

The neighbor checking logic checks, not just the squares we can reach in one step, N E S W, but also the corners NE SE SW NW. Interesting. What about how player and monsters move?

I find this:

function Monster:basicMoveRandomly(ignoredDungeon)
    local moves = {vec2(-1,0), vec2(0,1), vec2(0,-1), vec2(1,0)}
    local move = moves[math.random(1,4)]
    local tile = self:getTile():legalNeighbor(self,move)
    tile:moveObject(self)
end

Here we have the four directions, in yet another order, but no matter. If we’re on a hex map, moves needs to be a different set.

How should we best access the directions? I see at least these possibilities:

  • The Tile, if you have one, has a MapPoint, and a MapPoint knows whether or not it isHex. MapPoint could have a method directions returning the right set. Maybe there’s another for allDirections that returns the square corners?
  • The dungeon has the Map, which knows mapType, which is cartesian or hex, which we could use to get the directions.

A MapPoint does not know the map. It is a “pure” coordinate, independent of any particular space. So it cannot ask the Map for directions, even if we wanted it to. Similarly, the Map has all the points, but it doesn’t have a particular one, so it cannot readily ask a specific Point.

We do have the class MapDirection, which includes the methods we want, untested:

function MapDirection:init(x,y,z)
    self.x = x
    self.y = y
    self.z = z
end

function MapDirection:hexDirection(n)
    assert(1 <= n and n <= 6, "Impossible hex direction "..n)
    return MapDirection:hexMapDirections()[n]
end

function MapDirection:cartesianDirection(n)
    assert(1 <= n and n <= 4, "Impossible cartesian direction "..n)
    return MapDirection:cartesianMapDirections()[n]
end

function MapDirection:hexMapDirections()
    return{ MapDirection(1,-1,0), MapDirection(1,0,-1), MapDirection(0,1,-1),MapDirection(-1,1,0), MapDirection(-1,0,1), MapDirection(0,-1,1) }
end

function MapDirection:cartesianMapDirections()
    return {MapDirection(1,0,0), MapDirection(0,0,1), MapDirection(-1,0,0), MapDirection(0,0,-1)}
end

I’m thinking a class method on MapPoint, conditioned by isHex, at least for now. We’ll drive it down to MapDirection, of course. Let’s TDD it, just to get a feel for how it might be used as well as to add some tests.

        _:test("MapPoint Directions", function()
            local sq = MapPoint:cartesian(1,1)
            local dirs = sq:directions()
            _:expect(#dirs).is(4)
            _:expect(dirs[1]).is(MapDirection(1,0,0))
        end)

This seems like a good start.

1: MapPoint Directions -- TestMapClasses:17: attempt to call a nil value (method 'directions')

So far so good.

function MapPoint:directions()
    if self.isHex then
        return MapDirection:hexMapDirections()
    else
        return MapDirection:cartesianMapDirections()
    end
end

I don’t like the if statement but let’s make it work, then make it right. Or bright. Whatever. Sweet, test runs. We can commit: MapPoint understands directions.

We do need to beef up that trivial test a bit:

        _:test("MapPoint Directions", function()
            local sq = MapPoint:cartesian(1,1)
            local dirs = sq:directions()
            _:expect(#dirs).is(4)
            _:expect(dirs[1]).is(MapDirection(1,0,0))
            local hx = MapPoint:hex(1,2,-3)
            dirs = hx:directions()
            _:expect(#dirs).is(6)
            _:expect(dirs[4]).is(MapDirection(-1,1,0))
        end)

I expected this to work. It did not:

1: MapPoint Directions  -- Actual: 4, Expected: 6
1: MapPoint Directions  -- Actual: MapDirection(0,0,-1), Expected: MapDirection(-1,1,0)

Looks to me as if I got the cartesian ones back again.

Let’s check the point. My best guess is that the flag isn’t set.

        _:test("MapPoint Directions", function()
            local sq = MapPoint:cartesian(1,1)
            local dirs = sq:directions()
            _:expect(#dirs).is(4)
            _:expect(dirs[1]).is(MapDirection(1,0,0))
            local hx = MapPoint:hex(1,2,-3)
            _:expect(hx.isHex,"isHex").is(true)
            dirs = hx:directions()
            _:expect(#dirs).is(6)
            _:expect(dirs[4]).is(MapDirection(-1,1,0))
        end)
1: MapPoint Directions isHex -- Actual: nil, Expected: true

Right. Is there no such flag, or what?

Oh, flag is _isHex. Used wrong name, twice.

function MapPoint:directions()
    if self._isHex then
        return MapDirection:hexMapDirections()
    else
        return MapDirection:cartesianMapDirections()
    end
end

...
            _:expect(hx._isHex,"isHex").is(true)
...

There go those underbar member names. I really want to use them, but my reflexes just aren’t there for them. Let’s make this work, then maybe change them yet again.

Test runs again. Commit: MapPoint correctly returns both hex and cartesian directions.

Before we go improving, let’s continue toward working. We need our Map to have the same capabilities. Extend the test. No, write a new one.

Start here:

        _:test("Map Directions", function()
            local NoClass = class()
            local hx = Map:hexMap(2,2,NoClass)
            local dirs = hx:directions()
            _:expect(#dirs).is(4)
            _:expect(dirs[2]).is(MapDirection(0,0,1))
        end)

Expect to fail on directions:

2: Map Directions -- TestMapClasses:31: attempt to call a nil value (method 'directions')

Whee! Now do it:

function Map:directions()
    return MapDirection:hexMapDirections()
end

Now fix the test you fool:

        _:test("Map Directions", function()
            local NoClass = class()
            local hx = Map:hexMap(2,2,NoClass)
            local dirs = hx:directions()
            _:expect(#dirs).is(6)
            _:expect(dirs[2]).is(MapDirection(1,0,-1))
        end)

I was copying off my own paper but not carefully enough. Now it works. Extend test:

        _:test("Map Directions", function()
            local NoClass = class()
            local hx = Map:hexMap(2,2,NoClass)
            local dirs = hx:directions()
            _:expect(#dirs).is(6)
            _:expect(dirs[2]).is(MapDirection(1,0,-1))
            local sq = Map:cartesianMap(2,2,NoClass)
            local dirs = sq:directions()
            _:expect(#dirs).is(4)
            _:expect(dirs[2]).is(MapDirection(0,0,1))
        end)

Expect failures and get them:

2: Map Directions  -- Actual: 6, Expected: 4
2: Map Directions  -- Actual: MapDirection(1,0,-1), Expected: MapDirection(0,0,1)

Improve the function:

function Map:directions()
    if self.mapType == cartesian then
        return MapDirection:cartesianMapDirections()
    else
        return MapDirection:hexMapDirections()
    end
end

OK, it works, at least well enough for now. But is it right?

It seems odd to me that the Map would use the name of a function (well, the identity of a function) to decide whether it’s cartesian or hex, while a Point would have a flag in it saying what it is. What concerns me? Well, first of all the flags are different. One is boolean, the other is a function. Second, it’s entirely possible to have them set opposite of each other. We could readily create a hex map and then start trying to use square mapPoints on it, and no one would stop us.

What might be better?

We could require that you have a Map in order to get Points, and let the Map decide what kind of Points you get. That might not be entirely convenient: in particular our tests won’t like that rule, but we can surely deal with that either by changing them or by having private testing methods or something like that. In actual use we might need to make a point in the absence of a map, but we could wait and see if that happens.

We could have a new object, MapStrategy or MapType or some such name, that controls what kind of things you get. We’d say MapStrategy:hex and thereafter all things are hex until further notice. To get a map you’d say MapStrategy:createMap() and to get a Point you could say MapStrategy:point().

One advantage to the strategy notion is that it would allow us to keep the other Map classes more pure: they wouldn’t have to know so much about each other. Another advantage, possibly, is that the rest of the program wouldn’t have to know so much about the classes Map and MapPoint and MapDirection. Maybe they’d just know about MapStrategy and its methods.

I’ve come to like the strategy idea. Let’s try it and see how it goes.

I envision at this earliest moment in the idea, two concrete classes and one object. The object is a global, MapStrategy, containing either an instance of HexMapStrategy or CartesianMapStrategy. Those two concrete classes implement all the methods we need, whatever they turn out to be.

To go this way, I think we’ll have to do a bit of conversion in the square-tiled game, but most of it should be straightforward.

I’d kind of wish that Map and MapPoint and MapDirection, and probably HexMapStrategy and CartesianMapStrategy could all be private names, known only among themselves. Instances would appear, of course, but they’d be dealt with abstractly in use. We’ll hold off on that until we get the thing built, but it might reduce complexity overall, and there are a cubic bunload of classes in Dung already.

I want to TDD MapStrategy’s classes, and while I’d rather like to do it over in the Spike, I think we’ve deviated far enough from the Spike as to consider it obsolete and needing deletion. So we’ll do it here in the big program.

Let’s at least get started. Here’s my start:

-- MapStrategy
-- RJ 20211013

local _strat

function testMapStrategyClasses()
    CodeaUnit.detailed = false
    
    _:describe("Test MapStrategy Classes", function()
        
        _:before(function()
            _strat = MapStrategy
        end)
        
        _:after(function()
            mapStrategy = _strat
        end)
        
        _:test("CartesianStrategy", function()
            
        end)
    end)
end

I’m sure I’m going to want to protect any official MapStrategy variable, and I want all the code that we write to use that global for access. I can imagine some more arcane, esoteric, and bizarre injection kind of thing, but at this moment I don’t see it.

Let’s see what we want to test. We want two concrete classes, HexMapStrategy and CartesianMapStrategy, with methods like createMap, directions and point. Maybe createPoint, we’ll decide. The MapStrategy object is only ever one or the other of those. I think we’ll create an instance of each class, not use the class directly, although we could surely do that or even provide a table of functions. We’re object-oriented here, and we try to stick with that pattern where we can.

If we get this right, the other Map classes won’t need their flags any more.

I guess I’ll just write something like a test and see what happens. This is basically off-the-cuff, but hopefully educated design.

        _:test("CartesianStrategy", function()
            MapStrategy = CartesianStrategy()
            local ms = MapStrategy
            local map = MapStrategy:map(2,3,FakeClass)
            
        end)

I get this far and ask myself what I want to test. Maybe how many tiles there are? That’s easy, I’ll do it, just to get rolling.

        _:test("CartesianStrategy", function()
            MapStrategy = CartesianStrategy()
            local ms = MapStrategy
            local map = MapStrategy:map(2,3,FakeClass)
            local ct = 0
            for k,t in map:pairs() do
                ct = ct + 1
            end
            _:expect(ct,"number of cells").is(6)
        end)

That’s surely enough to fail. We’ll follow our nose for a while:

1: CartesianStrategy -- MapStrategy:21: attempt to call a nil value (global 'CartesianStrategy')

We code:

CartesianStrategy = class()

function CartesianStrategy:init()
end

function CartesianStrategy:map(w,h,klass,...)
    return Map:cartesianMap(w,h,klass,...)
end

That looks like what we expect for a strategy object: it just forwards things, making minor translations along the way.

Now I haven’t checked the contents of the map at all, but at this level I don’t expect to have to. I could check its flag, but I’m hoping to get rid of the flag. Let’s instead get some directions.

        _:test("CartesianStrategy", function()
            MapStrategy = CartesianStrategy()
            local ms = MapStrategy
            local map = MapStrategy:map(2,3,FakeClass)
            local ct = 0
            for k,t in map:pairs() do
                ct = ct + 1
            end
            _:expect(ct,"number of cells").is(6)
            local dirs = map:directions()
            _:expect(#dirs).is(4)
        end)

This will run, we already have it implemented. But it isn’t implemented the way we want:

function Map:directions()
    if self.mapType == cartesian then
        return MapDirection:cartesianMapDirections()
    else
        return MapDirection:hexMapDirections()
    end
end

Instead we want this:

function Map:directions()
    return MapStrategy:directions()
end

Test to get the fail and be slightly surprised to get two:

2: Map Directions -- Map:62: attempt to index a nil value (global 'MapStrategy')
1: CartesianStrategy -- Map:62: attempt to call a nil value (method 'directions')

We’re going to have to revise those other tests. Irritating but nothing for it. Let’s get the second test there working and then revise the other tests: they should drive out methods just as well as a new test.

function CartesianStrategy:directions()
    return MapDirection:cartesianMapDirections()
end

I expect that to fix my current test, leave the other broken.

Yes:

2: Map Directions -- Map:62: attempt to index a nil value (global 'MapStrategy')

Go over there and improve these tests.

It is worth observing that this rework of the tests, while not difficult, is a bit tedious. But we are changing a fairly deep aspect of how things work. As we revise these tests, we can either provide them with the new Strategy idea, or let them go around the strategy and test the underlying methods, which will remain there most of the time. We are planning to remove the ones that check flags, but the primitives are there to be used if you’re an insider.

        _:before(function()
            _strat = MapStrategy
        end)
        
        _:after(function()
            MapStrategy = _strat
        end)
        
        _:test("MapPoint Directions", function()
            MapStrategy = CartesianStrategy()
            local sq = MapPoint:cartesian(1,1)
            local dirs = sq:directions()
            _:expect(#dirs).is(4)
            _:expect(dirs[1]).is(MapDirection(1,0,0))
            local hx = MapPoint:hex(1,2,-3)
            _:expect(hx._isHex,"isHex").is(true)
            dirs = hx:directions()
            _:expect(#dirs).is(6)
            _:expect(dirs[4]).is(MapDirection(-1,1,0))
        end)

Now this makes me a bit nervous, because maybe we don’t want people to know the class names CartesianMapStrategy and so on. For now, I’ll allow it.

I expect now to fail on the hex test not the square one.

2: Map Directions -- Map:62: attempt to index a nil value (global 'MapStrategy')

Hm, I was changing MapPoint Directions and it didn’t need the change yet. No harm done. Move down to the Map Directions test:

        _:test("Map Directions", function()
            MapStrategy = HexStrategy()
            local NoClass = class()
            local hx = Map:hexMap(2,2,NoClass)
            local dirs = hx:directions()
            _:expect(#dirs).is(6)
            _:expect(dirs[2]).is(MapDirection(1,0,-1))
            MapStrategy = CartesianStrategy()
            local sq = Map:cartesianMap(2,2,NoClass)
            local dirs = sq:directions()
            _:expect(#dirs).is(4)
            _:expect(dirs[2]).is(MapDirection(0,0,1))
        end)

This’ll fail for lack of HexStrategy, I reckon.

2: Map Directions -- TestMapClasses:33: attempt to call a nil value (global 'HexStrategy')

Implement it:

HexStrategy = class()

function HexStrategy:map(w,h,klass,...)
    return Map:hexMap(w,h,klass,...)
end

function HexStrategy:directions()
    return MapDirection:hexMapDirections()
end

Tests run. Now we want points to go through the strategy. Let’s change them in the test we’re currently looking at.

        _:test("MapPoint Creation", function()
            local coord = MapPoint:hex(0,0,0)
            _:expect(coord:invalid()).is(false)
            local f = function() MapPoint:hex(1,1,1) end
            _:expect(f).throws("Invalid HexCoordinates 1,1,1")
            f = function() MapPoint(1,0,-1) end
            _:expect(f).throws("Invalid call to MapPoint(1,0,-1)")
            coord = MapPoint:hex(1.0, 0, -1.0)
            _:expect(coord:key()).is("1,0,-1")
        end)

If this works as intended, we can rephrase this as:

        _:test("MapPoint Creation", function()
            MapStrategy = HexStrategy
            local ms = MapStrategy
            local coord = ms:point(0,0,0)
            _:expect(coord:invalid()).is(false)
            local f = function() ms:point(1,1,1) end
            _:expect(f).throws("Invalid HexCoordinates 1,1,1")
            f = function() MapPoint(1,0,-1) end
            _:expect(f).throws("Invalid call to MapPoint(1,0,-1)")
            coord = ms:point(1.0, 0, -1.0)
            _:expect(coord:key()).is("1,0,-1")
        end)

That should fail looking for point.

3: MapPoint Creation -- TestMapClasses:49: attempt to call a nil value (method 'point')

OK, that was on Hex, so …

function HexStrategy:point(x,y,z)
    return MapPoint:hex(x,y,z)
end

Test runs. We should be committing. Commit: interim release of working MapStrategy, HexMapStrategy, CartesianMapStrategy.

We could hold off, but there’s no reason to do so, no one is harmed by these things existing, and now I could go to lunch or something.

Which I’d like to do, it is now 1240 local. But we’ll press on a bit further.

Here’s a revised test:

        _:test("MapPoint Hex Arithmetic", function()
            MapStrategy = HexStrategy()
            local point = MapStrategy:point(0,0,0)
            local notDir = MapStrategy:point(1,0,-1)
            local f = function() return point + notDir end
            _:expect(f).throws("Attempt to add a MapPoint plus a non-MapDirection")
            local dir = MapDirection(1,0,-1)
            local result = point + dir
            _:expect(result).is(MapStrategy:point(1,0,-1))
            local result2 = result + dir
            _:expect(result2).is(MapStrategy:point(2,0,-2))
        end)

I expect it to work now, and I am wondering how we’ll handle map direction literals, if at all. Real code may never need them.

Tests still run, that was a hex test. Next test is cartesian:

        _:test("Cartesian Point Arithmetic", function()
            MapStrategy = CartesianStrategy()
            local point = MapStrategy:point(2,3)
            local notDir = MapStrategy:point(1,1)
            local f = function() return point + notDir end
            _:expect(f).throws("Attempt to add a MapPoint plus a non-MapDirection")
            local dir = MapDirection:cartesianDirection(1)
            local result = point + dir
            _:expect(result).is(MapStrategy:point(3,3))
        end)

I expect this to fail looking for point:

5: Cartesian Point Arithmetic -- TestMapClasses:74: attempt to call a nil value (method 'point')

I implement point, adding a diagnostic:

function CartesianStrategy:point(x,y,z)
    assert(z==nil, "Unexpected third value for cartesian point")
    return MapPoint:cartesian(x,y)
end

Test runs. Let’s test the error case:

        _:test("Cartesian Point Arithmetic", function()
            MapStrategy = CartesianStrategy()
            local point = MapStrategy:point(2,3)
            local notDir = MapStrategy:point(1,1)
            local f = function() return point + notDir end
            _:expect(f).throws("Attempt to add a MapPoint plus a non-MapDirection")
            local dir = MapDirection:cartesianDirection(1)
            local result = point + dir
            _:expect(result).is(MapStrategy:point(3,3))
            local f = function() return MapStrategy:point(1,2,3) end
            _:expect(f).throws("Unexpected third value for cartesian poinx")
        end)

I spelled “point” as “poinx” to see the test fail:

5: Cartesian Point Arithmetic  -- Actual: function: 0x288e3e190, Expected: Unexpected third value for cartesian poinx

Fix the typo.

Tests run. But I don’t feel these tests are well enough converted yet. I’ll go through them, resulting in lots of small changes. Skip over this but it’s here for the record:

-- RJ 20200911
-- Tests for Map and associated classes

local _strat

function testMapClasses()
    CodeaUnit.detailed = false
    
    _:describe("Test Map Classes", function()
        
        _:before(function()
            _strat = MapStrategy
        end)
        
        _:after(function()
            MapStrategy = _strat
        end)
        
        _:test("MapPoint Directions", function()
            MapStrategy = CartesianStrategy()
            local sq = MapStrategy:point(1,1)
            local dirs = sq:directions()
            _:expect(#dirs).is(4)
            _:expect(dirs[1]).is(MapDirection(1,0,0))
            MapStrategy = HexStrategy()
            local hx = MapStrategy:point(1,2,-3)
            _:expect(hx._isHex,"isHex").is(true)
            dirs = hx:directions()
            _:expect(#dirs).is(6)
            _:expect(dirs[4]).is(MapDirection(-1,1,0))
        end)
        
        _:test("Map Directions", function()
            MapStrategy = HexStrategy()
            local NoClass = class()
            local hx = MapStrategy:map(2,2,NoClass)
            local dirs = hx:directions()
            _:expect(#dirs).is(6)
            _:expect(dirs[2]).is(MapDirection(1,0,-1))
            MapStrategy = CartesianStrategy()
            local sq = MapStrategy:map(2,2,NoClass)
            local dirs = sq:directions()
            _:expect(#dirs).is(4)
            _:expect(dirs[2]).is(MapDirection(0,0,1))
        end)
        
        _:test("MapPoint Creation", function()
            MapStrategy = HexStrategy()
            local ms = MapStrategy
            local coord = ms:point(0,0,0)
            _:expect(coord:invalid()).is(false)
            local f = function() ms:point(1,1,1) end
            _:expect(f).throws("Invalid HexCoordinates 1,1,1")
            f = function() MapPoint(1,0,-1) end
            _:expect(f).throws("Invalid call to MapPoint(1,0,-1)")
            coord = ms:point(1.0, 0, -1.0)
            _:expect(coord:key()).is("1,0,-1")
        end)
        
        _:test("MapPoint Hex Arithmetic", function()
            MapStrategy = HexStrategy()
            local point = MapStrategy:point(0,0,0)
            local notDir = MapStrategy:point(1,0,-1)
            local f = function() return point + notDir end
            _:expect(f).throws("Attempt to add a MapPoint plus a non-MapDirection")
            local dir = MapDirection(1,0,-1)
            local result = point + dir
            _:expect(result).is(MapStrategy:point(1,0,-1))
            local result2 = result + dir
            _:expect(result2).is(MapStrategy:point(2,0,-2))
        end)
        
        _:test("Cartesian Point Arithmetic", function()
            MapStrategy = CartesianStrategy()
            local point = MapStrategy:point(2,3)
            local notDir = MapStrategy:point(1,1)
            local f = function() return point + notDir end
            _:expect(f).throws("Attempt to add a MapPoint plus a non-MapDirection")
            local dir = MapDirection:cartesianDirection(1)
            local result = point + dir
            _:expect(result).is(MapStrategy:point(3,3))
            local f = function() return MapStrategy:point(1,2,3) end
            _:expect(f).throws("Unexpected third value for cartesian point")
        end)
        
        _:test("Hex Screen Positions", function()
            MapStrategy = HexStrategy()
            local screenDiameter = 64
            _:expect(MapStrategy:point(0,0,0):screenPos(screenDiameter)).is(vec2(0,0))
            local oneUp = MapStrategy:point(1,-2,1):screenPos(screenDiameter)
            _:expect(oneUp.x).is(screenDiameter/2.0*1.5*math.sqrt(3), 0.01)
            _:expect(oneUp.y).is(screenDiameter/2.0*1.5, 0.01)
        end)
        
        _:test("Cartesian Screen Positions", function()
            MapStrategy = CartesianStrategy()
            local screenDiameter = 50
            local pt = MapStrategy:point(1,1)
            _:expect(pt:screenPos(screenDiameter)).is(vec2(25,25))
            local upAndOver = MapStrategy:point(3,2)
            _:expect(upAndOver:screenPos(screenDiameter)).is(vec2(125, 75))
        end)
        
        _:test("MapPoint hashing works", function()
            MapStrategy = HexStrategy()
            local tab = {}
            local c1 = MapStrategy:point(1,0,-1)
            _:expect(c1:key()).is("1,0,-1")
            local c2 = MapStrategy:point(1,-1,0)
            local c3 = MapStrategy:point(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)
        
        --[[
        east: (1,-1,0), northeast:(1,0,-1), northwest:(0,1,-1), west:(-1,1,0), southwest:(-1,0,1), southeast(0,-1,1)
        --]]
        
        _:test("Cartesian Point key", function()
            MapStrategy = CartesianStrategy()
            local pt = MapStrategy:point(1,2)
            _:expect(pt:key(), "key").is("1,0,2")
        end)
        
        _:test("Cartesian Map", function()
            MapStrategy = CartesianStrategy()
            local map = MapStrategy:map(10,15, Square)
            for z = 1,15 do
                for x = 1,10 do
                    local sq = map:atXYZ(x,0,z)
                    local point = sq:point()
                    _:expect(point:x()).is(x)
                    _:expect(point:y()).is(0)
                    _:expect(point:z()).is(z)
                end
            end
        end)
        
        _:test("Hex Map", function()
            MapStrategy = HexStrategy()
            local map = MapStrategy:map(10,15, Hex)
            for z = 1,15 do
                for x = 1,10 do
                    local hex = map:atXYZ(x,0,z)
                    local point = hex:point()
                    _:expect(point:x()).is(x)
                    _:expect(point:y()).is(-x-z)
                    _:expect(point:z()).is(z)
                end
            end
        end)
        
        _:test("Additional Parameters", function()
            MapStrategy = CartesianStrategy()
            local map = MapStrategy:map(2,2, ParmTile, 666, 777)
            local tile = map:atXYZ(1,0,1)
            _:expect(tile.p1).is(666)
            _:expect(tile.p2).is(777)
        end)
        
    end)
end

-- ParmTile
-- RJ 20211008

ParmTile = class()

function ParmTile:init(aPoint, p1, p2)
    self.point = aPoint
    self.p1 = p1
    self.p2 = p2
end

-- Square
-- RJ 20211002

Square = class()

function Square:init(aCartesianPoint)
    self.cartesianPoint = aCartesianPoint
end

function Square:point()
    return self.cartesianPoint
end

-- Hex
-- RJ 20210926

Hex = class()

function Hex:init(aPoint)
    self.hexPoint = aPoint
    self.fillColor = nil
end

-- instance

function Hex:__tostring()
    return "Hex("..tostring(self.coord)..")"
end

function Hex:boundaryLine(angleIndex)
    local rad = MapPoint:screenRadius()
    local bTop = rad*vec2(0.8660254038, 0.5) -- cos30 = 0.8660254038 = sqrt(3)/2, sin(30)
    local bBot = rad*vec2(0.8660254038, -0.5)
    local ang = angleIndex*math.pi/3.0
    local vTop = bTop:rotate(ang)
    local vBot = bBot:rotate(ang)
    return {vTop.x, vTop.y, vBot.x, vBot.y}
end

function Hex:x()
    return self:point():x()
end

function Hex:y()
    return self:point():y()
end

function Hex:z()
    return self:point():z()
end

function Hex:point()
    return self.hexPoint
end

function Hex:draw()
    pushMatrix()
    pushStyle()
    rectMode(CENTER)
    strokeWidth(1.0/self:getScale())
    local coord = self:screenPos()
    local cx = coord.x
    local cy = coord.y
    translate(cx,cy)
    for rot = 0,5 do
        local bl = self:boundaryLine(rot)
        line(table.unpack(bl))
    end
    self:drawFill(self.fillColor)
    text(self:point():key(),0,0)
    popStyle()
    popMatrix()
end

function Hex:getScale()
    return modelMatrix()[1]
end

function Hex:drawFill(aColor)
    if aColor then
        pushMatrix()
        pushStyle()
        stroke(aColor)
        fill(aColor)
        rectMode(CENTER)
        self:drawFilledRectangles()
        popStyle()
        popMatrix()
    end
end

function Hex:drawFilledRectangles()
    local rad = MapPoint:screenRadius()
    local w = rad*1.7320508076 -- sqrt(3)
    local h = rad*1.0
    strokeWidth(1)
    self:drawFlatRect(w,h)
    self:drawLowToHighRect(w,h)
    self:drawHighToLowRect(w,h)
end

function Hex:drawFlatRect(w,h)
    rect(0,0, w,h)
end

function Hex:drawLowToHighRect(w,h)
    pushMatrix()
    rotate(60)
    rect(0,0, w,h)
    popMatrix()
end

function Hex:drawHighToLowRect(w,h)
    pushMatrix()
    rotate(120)
    rect(0,0, w,h)
    popMatrix()
end

function Hex:fill(aColor)
    self.fillColor = aColor
end

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

function Hex:screenPos()
    return self.hexPoint:screenPos()
end

Commit: MapStrategy and test enhancements.

Time for reflection and a break.

Reflection

I don’t like the explicit usage:

MapStrategy = HexStrategy()

We do need some object to talk to. MapStrategy is too long a word to enjoy typing it much. I think I’d prefer, maybe, Maps. And I think that when you say HexStrategy(), or maybe HexMaps(), you’re in the mode and thereafter Maps will do the right thing, i.e. what MapStrategy does now.

That aside, I think I like what has happened. You just ask for a map, and provide parameters. You just ask for a point, ditto. You provide the right number of parameters for the situation but you don’t have to name the type.

I think that the tests above (you can look at them now if you want to) express what is going on in a simpler fashion that remains clear, and that doesn’t say so much about what kind of thing we’re dealing with. The type decisions are made behind the curtain.

It’s early days, and I already know I want to change it around a bit, but by and large, I think we can live with this setup a bit more readily than we could when we got here this morning.

The code is more habitable, and more expressive. Not perfect, just better. IMO, YMMV of course.

See you next time!