I’m disappointed, at least so far, that my ‘functional programming’ capability seems not to be helpful in the Dung[eon] program. But maybe there’s still hope.

I reported on a Slack which I frequent that I could find no credible uses for my new fp primitives. A colleague commented that they’d love to look at code that has a loop in it, where it seems that these primitives won’t help. I hope that they’ll have a look at yesterday’s fp article: we’ll learn something.

This morning, the last day of February and what seems like the ten-thousandth day of the Doldrums, I want to look at the program again, not just with an eye to the fp primitives but to see whether there are interesting things for me to learn after so long away from the program. (The prior article was written on October 20th, 2021.)

I am torn with regard to the Hex map that is part-way installed. I wasn’t enjoying the project, which is probably why I wandered off. Maybe I’ll just ignore it. Maybe I’ll rip it out. Maybe, but very unlikely, I’ll work on it.

One thing I will do is import the fp project and try using it in a few places. I sketched a few uses in the prior fp article but here we’ll complete a couple of uses to be sure we’re giving the fp a fair chance.

This program has 52 tabs. Each tab contains either at least one class, or tests for a class. So it’s a fairly large program to be dealing with. There are 139 tests, in 29 suites. Most of the tests have more than one “assert”. There are probably too few tests, because I couldn’t see how to test some things, or was too lazy, or not smart enough. But ti seems to be quite reliable, so management isn’t complaining.

Anyway let’s look around.

Starting with For Loops

function GameRunner:makeRoomsFromXYWH(roomTable, annTable)
    local dungeon = self:getDungeon()
    local result = {}
    for i,e in ipairs(roomTable) do
        table.insert(result, Room(e[1],e[2],e[3],e[4], self, true))
        if annTable[i] then
            result[#result]:announce(dungeon, annTable[i])
        end
    end
    return result
end

This is interesting. What’s going on here is a bit odd. We’re defining rooms based on an input array, roomTable, of arrays. The inner arrays contain the x,y coordinates of the new room, and the width and height. We’re creating a list of new rooms, with those parameters, in our result. There is a related table, annTable, and if there is an entry for the room we’re building (by index) we tell that room (the last one in the table) to announce. Let’s see if we can refactor that for clarity.

function GameRunner:makeRoomsFromXYWH(roomTable, annTable)
    local dungeon = self:getDungeon()
    local result = {}
    for i,e in ipairs(roomTable) do
        local rm = Room(e[1],e[2],e[3],e[4], self, true)
        table.insert(result, rm)
        if annTable[i] then
            rm:announce(dungeon, annTable[i])
        end
    end
    return result
end

If we could figure out a better way to do the announce thing, we could use reduce here.

Let’s do that assuming we can sort out the annTable thing somehow.

function GameRunner:makeRoomsFromXYWH(roomTable, annTable)
    local dungeon = self:getDungeon()
    local makeRoom = function(r,e) 
        table.insert(r,Room(e[1],e[2],e[3],e[4], self, true))
        return r 
    end
    local result = ar.reduce(roomTable, {}, makeRoom)
    -- do something about annTable.
    return result
end

This passes tests and makes the rooms correctly. the announcements are not set up, of course.

Now, as provided the annTable is a sparse collection. In our actual use, it only contains one element, at 2, for room 2.

I can do this to make it work:

function GameRunner:makeRoomsFromXYWH(roomTable, annTable)
    local dungeon = self:getDungeon()
    local makeRoom = function(r,e) 
        table.insert(r,Room(e[1],e[2],e[3],e[4], self, true))
        return r 
    end
    local result = ar.reduce(roomTable, {}, makeRoom)
    for i,ann in pairs(annTable) do
        result[i]:announce(dungeon,ann)
    end
    return result
end

This works. Let’s refactor to pull out two methods. In doing that I notice that dungeon isn’t needed in the room creation. Good to know. This function should really be named makeRoomsWithAnnouncements, shouldn’t it? Anyway for now …

function GameRunner:makeRoomsFromXYWH(roomTable, annTable)
    local rooms = self:makeXYWHRooms(roomTable)
    local dungeon = self:getDungeon()
    for i,ann in pairs(annTable) do
        rooms[i]:announce(dungeon,ann)
    end
    return rooms
end

function GameRunner:makeXYWHRooms(roomTable)
    local makeRoom = function(r,e) 
        table.insert(r,Room(e[1],e[2],e[3],e[4], self, true))
        return r 
    end
    return ar.reduce(roomTable, {}, makeRoom)
end

That works. Extract the other bit:

function GameRunner:makeRoomsFromXYWH(roomTable, annTable)
    local rooms = self:makeXYWHRooms(roomTable)
    self:addRoomAnnouncements(rooms, annTable)
    return rooms
end

function GameRunner:addRoomAnnouncements(rooms, annTable)
    local dungeon = self:getDungeon()
    for i,ann in pairs(annTable) do
        rooms[i]:announce(dungeon,ann)
    end
end

function GameRunner:makeXYWHRooms(roomTable)
    local makeRoom = function(r,e) 
        table.insert(r,Room(e[1],e[2],e[3],e[4], self, true))
        return r 
    end
    return ar.reduce(roomTable, {}, makeRoom)
end

Well, our code is more expressive of what we’re doing. I think we could do better by calling both our new methods from where the original method is called. And maybe we could create a better structure for the announcements. That would be a valuable thing for our Making App, if we had one, so that a level designer could have a single data structure for the room and any announcements. Let’s have a look at how we use this:

function GameRunner:createLearningRooms()
    local w = 12
    local h =  8
    local a2 = { "In this room, there is a health power-up.",
        "Please walk next to it (not onto it)",
        "and press the ?? button.",
    "You'll get an informative message about whatever you're next to." }
    local t = {
        {2,2, w,h},
        {15,2, w,h, a2},
        {28,2, w,h},
        {41,2, w,h},
        {2,11, w,h},
        {15,11, w,h},
        {28,11, w,h},
        {41,11, w,h},
        {2,20, w,h},
        {15,20, w,h},
        {28,20, w,h},
        {41,20, w,h},
    }
    for i,r in ipairs(t) do
        r[1] = r[1]+24
        r[2] = r[2]+24
    end
    self.rooms = self:makeRoomsFromXYWH(t, announcements)
    local r2 = self.rooms[2]
    local lootTile = r2:tileAt(self.dungeon, 2,2)
    local loot = Loot(lootTile, "health", 5,5)
    loot:setMessage("This is a Health Power-up of 5 points.\nStep onto it to add it to your inventory.")
end

OK, let’s do this. Let’s make our XYWH array into an XYWHA array, with an optional announcement.

    local a2 = { "In this room, there is a health power-up.",
        "Please walk next to it (not onto it)",
        "and press the ?? button.",
    "You'll get an informative message about whatever you're next to." }
    local t = {
        {2,2, w,h},
        {15,2, w,h, a2},
        {28,2, w,h},
        {41,2, w,h},
        {2,11, w,h},
        {15,11, w,h},
        {28,11, w,h},
        {41,11, w,h},
        {2,20, w,h},
        {15,20, w,h},
        {28,20, w,h},
        {41,20, w,h},
    }

Let’s now make that work, back in our room maker:

function GameRunner:makeXYWHRooms(roomTable)
    local makeRoom = function(r,roomDef)
        local x,y,w,h,ann = table.unpack(roomDef)
        local rm = Room(x,y,w,h, self, true)
        if ann then rm:announce(self:getDungeon(),ann) end
        table.insert(r,rm)
        return r 
    end
    return ar.reduce(roomTable, {}, makeRoom)
end

That works, and we don’t need the separate announcement method now. And now we can move the code here back up into the original method:

function GameRunner:makeRoomsFromXYWHA(roomTable, annTable)
    local makeRoom = function(r,roomDef)
        local x,y,w,h,ann = table.unpack(roomDef)
        local rm = Room(x,y,w,h, self, true)
        if ann then rm:announce(self:getDungeon(),ann) end
        table.insert(r,rm)
        return r 
    end
    return ar.reduce(roomTable, {}, makeRoom)
end

So is that better? Yes. What would be better still?

Well, the announcement, if any, should be part of the room creation. We see even in our rudimentary setup table that it’s natural to associate the message with the rest of the room definition. Let’s see what room creation looks like:

function Room:init(x,y,w,h, runner, paint)
    if paint == nil then paint = true end
    self.x1 = x
    self.y1 = y
    self.x2 = x + w - 1
    self.y2 = y + h - 1
    self.runner = runner
    if paint then
        self:paint()
    end
end

Let’s just allow another optional parameter for the announcement:

function Room:init(x,y,w,h, runner, paint, announcement)
    if paint == nil then paint = true end
    self.x1 = x
    self.y1 = y
    self.x2 = x + w - 1
    self.y2 = y + h - 1
    self.runner = runner
    if paint then
        self:paint()
    end
    if announcement then
        self:announce(runner:getDungeon(), announcement)
    end
end

With this in place we can do this:

function GameRunner:makeRoomsFromXYWHA(roomTable)
    local makeRoom = function(r,roomDef)
        local x,y,w,h,ann = table.unpack(roomDef)
        local rm = Room(x,y,w,h, self, true, ann)
        table.insert(r,rm)
        return r 
    end
    return ar.reduce(roomTable, {}, makeRoom)
end

This would be much better if we could insert to a table and return it, or if we could say something like

return result + Room(...)

I wonder whether we could provide something like that in our fp functions. Certainly should be possible. We’ll come back to that.

Why can’t the room create itself from our table? Why do I have to do all this work?

function GameRunner:makeRoomsFromXYWHA(roomTable)
    local makeRoom = function(r,roomDef)
        table.insert(r, Room:fromXYWHA(roomDef, self))
        return r 
    end
    return ar.reduce(roomTable, {}, makeRoom)
end

Needs a new class method, of course:

function Room:fromXYWHA(xywhaTable, runner)
    local x,y,w,h,a = table.unpack(xywhaTable)
    return Room(x,y,w,h, runner, true, a)
end

This works. Let me commit, this is good stuff. Commit: refactor creation of learning level.

Let’s pull our heads out and think about what has happened.

Retro

Using the desire to see why the fp primitives weren’t helpful, I tried to plug one in. It would go in partially, so I refactored the original method into two and did one part with reduce. Along the way we discovered that Lua’s use of table.insert, which doesn’t even return the table, make writing our reduction function awkward.

The other odd thing was that we broke a single loop that did two things into two loops each of which did one thing. Then we noticed that we wouldn’t have to loop at all if we included the announcement as part of the table that defines the room layout … and that’s a natural thing to want to do anyway, when we’re defining a level manually.

That led to recognizing the need for a new creation parameter and ultimately a new class method for creating a room from xywha information.

The net result, so far, is much simpler code in the making of the learning level, plus some not unreasonable enhancements to the methods of Room.

So this was a fair amount of time, couple of hours at least, for a small improvement. In a real product it would likely be of value, because we would be creating a “Making App” to allow level designers to lay out levels easily.

And we got an idea for improving the reduce function if it’s possible. Let’s try that now:

Improve reduce

Let’s write what we’d like to write, then make it work:

function GameRunner:makeRoomsFromXYWHA(roomTable)
    local makeRoom = function(result,roomDef)
        return result + Room:fromXYWHA(roomDef, self)
    end
    return ar.reduce(roomTable, Array{}, makeRoom)
end

Note that I pass in an Array to the reduce, not just a bare {}. If we run now that we’re going to get a complaint about +, I think. Yes, “attempt to perform arithmetic on a table”.

Let’s do this in the fp project:

        _:test("table + ", function()
            local ary = Array{}
            local ary2 = ary + {123,456}
            _:expect(ary2).is(ary)
            _:expect(ary[1][1]).is(123)
        end)
        
        _:test("reduce shorthand", function()
            local square = function(result,value)
                return result + {value, value*value}
            end
            local input = {1,3,5}
            local squares = ar.reduce(input,Array{},square)
            local mid = squares[2]
            _:expect(mid[1]).is(3)
            _:expect(mid[2]).is(9)
            -- but see map test "map builds the table better"
        end)

And …

ar.__add = function(tab,elt)
    table.insert(tab,elt)
    return tab
end

This is the metamethod for +. It’s called with both of the parameters to +, as shown there, so we just add our element to the table and return it.

Test runs. Commit D2: refactor to use Array + in makeRoomsFromXYWHA.

Sadly, as things stand, we can’t make a corresponding change to the kv form of the reduce, because, as I look at it now, reducing to a dictionary (kv table) doesn’t seem to make sense at all. I’ll have to explore that in a further fp article. I must be doing something wrong over there.

Be that as it may, we seem to have improved a bit of code substantially, by almost forcing the use of reduce and then refactoring, following our nose, until things got better.

Kind of odd. Must try this in another place and see what happens. Meanwhile, I need to figure out what some other language that offers these fp primitives does to dictionaries. It seems like I’m missing some understanding.

Nothing new about that. There’s always something to learn, or to learn better.

See you next time!