I’m at a loss to find anything interesting to do. But I bet I can find some code that could be improved. Thinking about an idea from Bruce Onder leads in an interesting direction.

In my long history of programming, nigh on to sixty years man and boy, I’ve always become less interested during the 90 percent of the development where you’re just plugging away, same old same old. One nice thing about an iterative / incremental approach is that every iteration can include all the ingredients that make up software development, analysis, design, testing, programming, code improvement … whatever elements you see in software, they’re all there in every iteration.

So I’m sure we can find something.

Oh, here in the mail bin we have an idea from Bruce Onder, via Twitter. Bruce was wondering whether it would be better to use the Bus publish/subscribe for things like the Trigger stack is doing, with Triggers just saying they were activated and some keyword, and various listeners listening for a Trigger shouting their name. Then you could have Announcers that published, and Cachers saving the messages.

Bruce quickly noticed the issue with multiple messages, which is why we cache the whole Announcer, but I think that could be managed by the Cacher having an overriding message that it could send, that would bypass the one-shot aspect.

We have what we have for two important reasons:

First, it’s based on what I first thought of, which was an Announcer that could be placed into more than one tile, back when that was possible. Most of what is there now is incremental evolution from that basic idea. We stepped from a unique proxy holding a pointer to one common Announcer, to the more elaborate design we now have.

Second, we have that more elaborate design, which we unquestionably did not need yet, because I had the idea for a nifty new more general structure, and I wanted to try it. And, I have to admit, I’m kind of happy how it turned out.

But it is over-designed for the current situation, and if I gave advice, which I do not, I’d advise avoiding over-design. It slows down feature progress, and that puts the entire effort in jeopardy.

Now there’s another issue, and it’s kind of a personal problem: I am used to designs where when things talk to each other, they have a pointer to whomever they want to talk with. The publish-subscribe model is interesting–as we’ll explore below–but it has a kind of mysterious character. We have two or more objects collaborating, and yet they have no knowledge of each other. You just shout “Hey!” and somewhere out there, listeners do the right thing.

Code like that, especially with our current publish/subscribe, is a bit hard to figure out: it’s not easy to find who subscribes to your cries.

Another issue is that publish/subscribe is good for an outgoing command, but it’s not so good for a real conversation. When we want the crawl to add some text, it’s fine to publish a command. But when we want to know where the princess is, we can’t get a return back from the publish. We could make whoever knows do another publish, or even call a method on us, but either way, it would break the more obvious flow of send message / get response / carry on.

In modern languages, http calls now often act like there was no delay between request and response. That’s managed by magic behind the scenes, essentially coroutines, where the http code saves our return info, and when the message comes back, dumps us right back into whatever function did the original http call.

I have no doubt that we could implement that. It might be fun to do so, and it might even be useful. One of the issues with the current design of the system is that it is highly connected, with GameRunner knowing dungeons and tiles and at least some of the dungeon objects and entities, and the tiles knowing their contents, and everyone who wants to know anything needing access to the GameRunner … and so on.

But suppose …

Suppose we had a pure publish / subscribe model–no, it is too much–suppose we used publish / subscribe much more than we do now. And, at least for now, suppose we could even have a “request” kind of publish that could return a value, or values, from all those who subscribed to that request.

Our objects generally don’t care who calls them, and they generally have no particular concern over the order of calls. They just do their thing and return their answer if there is one. So it just might be that the objects wouldn’t have to change much. Maybe they’d have to publish their answer rather than return it. Maybe we could even capture the returns: after all, the Bus does do regular method calls to the subscribers:

function EventBus:publish(event, optionalSender, optionalInfo)
    for subscriber,method in pairs(self:subscriptions(event)) do
        method(subscriber, event, optionalSender, optionalInfo or {})
    end
end

Nothing would stop us from saving all the results, something like this:

function EventBus:publish(event, optionalSender, optionalInfo)
    local results = {}
    for subscriber,method in pairs(self:subscriptions(event)) do
        table.insert( results, method(subscriber, event, optionalSender, optionalInfo or {}))
    end
    -- magically return table to caller.
end

The request form, of course, would have to include the caller’s id, so that we could get back to them. But it would be entirely doable.

Wait. It never occurred to me that it was that easy to manage a return. We might not need coroutines at all. Our code is never really asynchronous, even with the Bus. (Timing tweens might be an exception.) Consider this flow:

function Monster:basicMoveAwayFromPlayer(dungeon)
    local tiles = dungeon:availableTilesFurtherFromPlayer(self:getTile())
    self:basicRandomMove(tiles)
end

function Dungeon:availableTilesFurtherFromPlayer(aTile)
    local desiredDistance = self:manhattanToPlayer(aTile) + 1
    return self:availableTilesAtDistanceFromPlayer(aTile, desiredDistance)
end

function Dungeon:availableTilesAtDistanceFromPlayer(startingTile, desiredDistance)
    local targetTile = self:playerTile()
    return self:availableTilesAtDistanceFromTile(startingTile, targetTile, desiredDistance)
end

function Dungeon:playerTile()
    return DungeonContents:getTile(self:getPlayer())
end

function Dungeon:getPlayer()
    return self.runner:getPlayer()
end

function GameRunner:getPlayer()
    return self.player
end

function DungeonContentsCollection:getTile(object)
    return self.contentMap[object]
end

That code requires a number of direct object links in member variables, or globals:

reference user type
dungeon Monster member
DungeonContents Dungeon global
self.runner Dungeon member
self.player GameRunner member
self.contentMap Contents member

I’ve probably missed some.

Now I’m used to this but it’s not ideal, because some of these pointers aren’t “downward”. It’s one thing for an object to know its subordinate objects: the dungeon knows the tiles: that’s its point. But if the tiles know the dungeon–if the pointers point “upward”, that’s not so good. Connections are circular, which is a major issue if we ever want to disassemble things, and it’s also more difficult to set up tests, because we have to build up a legitimate set of surroundings just to test some fairly detailed aspect of some random object.

Now, the flow above really does need to know all those things. It needs to know which tiles around the Monster are further from the Player than current. Since the Monster can only move one space, we look for current distance plus one. We have to know where the player is to do that. We have to know which tiles are available to move onto.

Therefore, given that we use object pointers, there must be a path from monster to dungeon, to player, to the tiles. We could imagine that the dungeon would know the player, but it probably uses the runner for other things.

Would a request / publish / subscribe model allow us to reduce this coupling? Could we get to the point where no one needed to know any other object “above” it, only knowing the objects it legitimately holds? Could our objects wind up more atomic, more easily testable?

Verrry Interesting …

I don’t much care about the Announcer topic, but thinking about Bruce’s idea for Announcer, and loosening up our mind a bit, raises a fascinating design topic. Would we have a more habitable design this way?

Let’s look at the dark side for a moment, with our same example. At the level of the Monster, we just want to move to a tile that’s further from the Player, if we can. We’ve seen most of how that happens, with a few direct calls to the dungeon, and to the runner. The rest is internal code.

With request / publish / subscribe, GameRunner would subscribe to calls like “getPlayer”, but what we really wanted was the Player’s tile. So maybe we request “getPlayerTile” and the player returns her getTile directly. She references a global, DungeonContents to get that. In r/p/s she’d request “getTile(self)”. We’d replace a tiny amount of code, essentially little more than a hash access, with that loop building a table of contents. Or, maybe the request would know there’d be only one response. But it still has to find the subscribers and call them / it.

I was taught that when a design discussion has gone on for more than ten minutes, the code needs to participate in the discussion, in the form of an experiment. I usually extend the time period a bit, but the point remains: we need a more concrete understanding of what it would take to do this, and what the impact would be. So, let’s experiment.

Experiment

Shall we experiment inside Dungeon, or in a separate project? Let’s do a separate project, but we’ll bring over the EventBus as it stands. It includes tests, and they all run:

-- EventBus
-- RJ 20210414

local OldBus
local itHappened
local info

function testEventBus()
    CodeaUnit.detailed = false
    
    _:describe("EventBus publish/subscribe", function()
        
        _:before(function()
            OldBus = Bus
            Bus = EventBus()
            itHappened = {}
            info = { info=58008 }
        end)
        
        _:after(function()
            Bus = OldBus
        end)
        
        _:test("hookup", function()
            _:expect(EventBus).isnt(nil)
        end)
        
        _:test("can subscribe to a message", function()
            local listener = FakeListener("it happened")
            Bus:publish("it happened", 7734, info)
            _:expect(itHappened).has(listener)
        end)
        
        _:test("two subscribers each get message", function()
            local lis1 = FakeListener("it happened")
            local lis2 = FakeListener("it happened")
            Bus:publish("it happened", 7734, info)
            _:expect(itHappened).has(lis1)
            _:expect(itHappened).has(lis2)
        end)
        
        _:test("can subscribe to different events", function()
            local lis1 = FakeListener("it happened")
            local lis2 = FakeListener("something else happened")
            Bus:publish("it happened", 7734, info)
            _:expect(itHappened, "lis1 didn't get message").has(lis1)
            _:expect(itHappened, "lis2 received wrong message").hasnt(lis2)
        end)
        
        _:test("can unsubscribe", function()
            local lis1 = FakeListener("it happened")
            Bus:publish("it happened", 7734, info)
            _:expect(itHappened).has(lis1)
            Bus:unsubscribeAll(lis1)
            itHappened = {}
            Bus:publish("it happened", 7734, info)
            _:expect(itHappened, "lis1 was unsubscribed, got message").hasnt(lis1)
        end)
        
    end)
end

FakeListener = class()

function FakeListener:init(event)
    Bus:subscribe(self, self.happened, event)
    self.event = event
end

function FakeListener:happened(event, sender, info)
    if sender == 7734 and info.info == 58008 then
        itHappened[self] = self
    end
end

EventBus = class()

function EventBus:init()
    self:clear()
end

function EventBus:clear()
    self.events = {}
end

function EventBus:subscribe(listener, method, event)
    assert(method, "no method")
    local subscriptions = self:subscriptions(event)
    subscriptions[listener] = method
end

function EventBus:publish(event, optionalSender, optionalInfo)
    for subscriber,method in pairs(self:subscriptions(event)) do
        method(subscriber, event, optionalSender, optionalInfo or {})
    end
end

function EventBus:subscriptions(event)
    local subscriptions = self.events[event]
    if not subscriptions then
        subscriptions = {}
        self.events[event] = subscriptions
    end
    return subscriptions
end

function EventBus:unsubscribeAll(subscriber)
    for event,subscriptions in pairs(self.events) do
        subscriptions[subscriber] = nil
    end
end

function EventBus:dump()
    print("Bus dump", self)
    for event, subs in pairs(self.events) do
        print("Event ", event)
        for listener, method in pairs(subs) do
            print("listener ", listener)
        end
    end
end

Now what? I think I’d like to have some timing tests, so I think I’ll just add another tab with some objects and we’ll see what we get. This is an experiment, so stand back.

Here’s the direct code:

Monster = class()

function Monster:runTiming()
    local runner = Runner()
    local dungeon = Dungeon(runner)
    local monster = Monster(dungeon)
    local time = os.time()
    for j = 1,10 do
        for i = 1,1000000 do
            monster:chooseMove()
        end
    end
    print(os.difftime(os.time(),time))
end

function Monster:init(dungeon)
    self.dungeon = dungeon
end

function Monster:chooseMove()
    self.dungeon:getTiles()
end

Dungeon = class()

function Dungeon:init(runner)
    self.runner = runner
end

function Dungeon:getTiles()
    self.runner:getPlayer()
    for i = 1,20 do
        local j = i*i
    end
    return {1,2,3,4,5}
end

Runner = class()

function Runner:init()
    self.player = 1234
end

function Runner:getPlayer()
    return self.player
end

The test is running Monster:chooseMove ten million times. The test runs in 4 seconds on my iPad. (Time is only good to seconds, for reasons.)

Now let’s try the publish / subscribe form. We’ll probably bash some code into the Bus at some point but for now we can “just” do something like this:

Monster = class()

function Monster:runTiming()
    local runner = Runner()
    local dungeon = Dungeon(runner)
    local monster = Monster(dungeon)
    local time = os.time()
    for j = 1,10 do
        for i = 1,1000000 do
            monster:chooseMove()
        end
    end
    print(os.difftime(os.time(),time))
end

function Monster:init(dungeon)
    self.dungeon = dungeon
end

function Monster:chooseMove()
    --self.dungeon:getTiles()
    Bus:publish("getTiles")
end

Dungeon = class()

function Dungeon:init(runner)
    self.runner = runner
    Bus:subscribe(self, self.getTiles, "getTiles")
end

function Dungeon:getTiles()
    --self.runner:getPlayer()
    Bus:publish("getPlayer")
    for i = 1,20 do
        local j = i*i
    end
    return {1,2,3,4,5}
end

Runner = class()

function Runner:init()
    self.player = 1234
    Bus:subscribe(self, self.getPlayer, "getPlayer")
end

function Runner:getPlayer()
    return self.player
end

All that happens is that callers do publish, receivers do subscribe. Everyone assumes that results will come back. Of course, we’re not returning any results. But we are executing the rest of the loop. Let’s bash a return table into Bus:

function EventBus:publish(event, optionalSender, optionalInfo)
    local result = {}
    for subscriber,method in pairs(self:subscriptions(event)) do
        local res = method(subscriber, event, optionalSender, optionalInfo or {})
        table.insert(result,res or "none")
    end
    return result
end

And let’s print the resulting tiles table:

function Monster:chooseMove()
    --self.dungeon:getTiles()
    local tiles = Bus:publish("getTiles")
    self:printTiles(tiles)
end

function Monster:printTiles(tiles)
    for i,t in ipairs(tiles) do
        for j,tt in ipairs(t) do
            print(i,j,tt)
        end
    end
end

Whoa! I almost printed that ten million times. That might be bad.

I’ll comment out the loop: I just want to see if I get the results back. And I do. Now I’ll comment that out and run the timing again.

OK, here’s the final form of the code. I put some actual processing in, mostly to make the printed result more interesting:

Monster = class()

function Monster:runTiming()
    local runner = Runner()
    local dungeon = Dungeon(runner)
    local monster = Monster(dungeon)
    local time = os.time()
    for j = 1,1 do
        for i = 1,1000000 do --]]
            monster:chooseMove()
        end
    end--]]
    print(os.difftime(os.time(),time))
end

function Monster:init(dungeon)
    self.dungeon = dungeon
end

function Monster:chooseMove()
    --self.dungeon:getTiles()
    local tiles = Bus:publish("getTiles")
    --self:printTiles(tiles)
end

function Monster:printTiles(tiles)
    for i,t in ipairs(tiles) do
        for j,tt in ipairs(t) do
            print(i,j,tt)
        end
    end
end

Dungeon = class()

function Dungeon:init(runner)
    self.runner = runner
    Bus:subscribe(self, self.getTiles, "getTiles")
end

function Dungeon:getTiles()
    local tiles = {}
    --self.runner:getPlayer()
    Bus:publish("getPlayer")
    for i = 1,20 do
        local j = i*i
        table.insert(tiles,"tile "..j)
    end
    return tiles
end

Runner = class()

function Runner:init()
    self.player = 1234
    Bus:subscribe(self, self.getPlayer, "getPlayer")
end

function Runner:getPlayer()
    return self.player
end

You can see the two places where you comment/uncomment to switch it from Bus mode to direct mode. Also you may not, I reduced the outer loop to 1, because the additional processing slowed it down substantially.

The results: direct: 5 seconds, bus: 6 seconds. I think we need to run the long test, now that I’m sure it actually terminates. I’ll do 5 cycles, not 10.

Bus: 27 seconds; Direct: 22 seconds. Bus is about 22% worse. And the difference in real time is about 1 microsecond, since we did 5 million iterations in an additional 5 seconds.

Conclusion: Speed will not be an issue if we want to use publish / subscribe more broadly.

Other Concerns

On the one hand, it’s pretty easy to see that the code won’t be very different. Instead of

    local player = self.runner:getPlayer()

We’ll write:

    local player = Bus:publish("getPlayer")

I do have one concern, which is the possibility that we’ll instead say something like:

    local player = Bus:publish("getPIayers")

No one will be subscribing to “getPlayers”. Well, that’ll just mean that player comes back nil or empty table, whatever we decide the protocol is. So the code will explode at roughly the same time as before.

Additionally we might want to have the EventBus know all the events that are legal, so that it can object when someone subscribes to or publishes an unknown one. It’d be a small hassle to introduce a new event before using it, but just a small one.

Tempting

This is tempting, isn’t it? We have no substantial reason to do it, although the difficulty of testing is always with us, and this might improve that situation. From the viewpoint of learning about “large” refactorings, it would be educational, so there’s always that option.

We can clearly do this as incrementally as we wish. Once a given object is set up to subscribe to things, it does nothing different from before. Then other objects have the luxury of calling it directly or via the bus. We might set the rule that using direct calls is to be temporary, or we could allow it from owner to owned, and save the remote call for distant friends.

We can even defer that decision, because direct and Bus calls will work transparently from the get-go.

Fascinating. Just goes to show what happens when you take a weird idea and push it to the limits. Sometimes it breaks down, but sometimes you get something that can be useful.

Will we do this? I suspect we will, just to discover how a design feels that’s more indirect and less coupled.

Besides, I might get in big trouble. That’s always worth a laugh.

The Big Question

The Big Question is: are we going to start right now, or are we going to give the idea time to gel a bit in our minds?

I guess I wouldn’t ask if the answer were “no”. But let’s at least see if we can put in a new feature while we’re at it.

Here’s a random idea. What if some rooms are dark, so that you can only see a couple of tiles (unless you have a light source).

Let’s first try a simple experiment, changing the limits of illumination:

function Tile:illuminateLine(dx,dy)
    local max = 8
    local pts = Bresenham:drawLine(0,0,dx,dy)
    for i,offset in ipairs(pts) do
        local pos = self:pos() + offset
        local d = self:pos():dist(pos)
        if d > max then break end
        local tile = self.runner:getTile(pos)
        tile:setVisible(d)
        if tile.kind == TileWall then break end
    end
end

This function is called repeatedly at various angles to illuminate tiles. We can change max to, say, 3, and see what it looks like.

moving around with illumination distance 3

I don’t exactly like the rectangular shape of the displayed area but you may recall that we used to have a tint function that darkened things more incrementally. We could perhaps put that back.

I wonder how we’d control something like this. It’s “easy” to see that we could use our Lever as a light switch, setting the illumination distance larger as you push the switch to the right. But how would we properly deal with one room being darker than another adjacent one?

Maybe we’d ring the room with Triggers, but that would probably look strange. As you came down the hall you could see into the room but when you get there you couldn’t.

hallway view shows many tiles in upcoming room

Belay This Idea

OK, we’re not gonna go there this morning. It’s possibly a good idea to have dark rooms, but at this moment I don’t see how to put in a useful first cut.

Well, wait. In the Learning Level, let’s try something.

We’ll have a new kind of DungeonObject, Darkness. (I expect it won’t need to inherit from DO for long.) I’m gonna do this without tests. We’ll see if that’s a bad idea.

Darkness = class(DungeonObject)

function Darkness:init()
end

function Darkness:actionWithPlayer(aPlayer)
    Bus:publish("Darkness")
end

function Darkness:draw()
end

We need to put it into TileArbiter (that’s a pain, isn’t it?)

    t[Darkness][Monster] = {moveTo=TileArbiter.acceptMove}
    t[Darkness][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithDarkness}

Player needs startActionWithDarkness:

function Player:startActionWithDarkness(aDarkness)
    aDarkness:actionWithPlayer(self)
end

Now we need to have this actually change that value in the illumination somehow. That’s back in Tile. But we don’t want every tile responding to this message. Only one object needs to do this, though we need a place to store the max.

local IlluminationLimit = 8

function Tile:illuminateLine(dx,dy)
    local max = IlluminationLimit
    local pts = Bresenham:drawLine(0,0,dx,dy)

A class method on Tile to set it:

function Tile:illuminationLimit(limit)
    IlluminationLimit = limit
end

And someone to subscribe and send the message. I nominate GameRunner:

function GameRunner:init(testX, testY)
    self.tileSize = 64
    self.tileCountX = testX or 85 -- if these change, zoomed-out scale 
    self.tileCountY = testY or 64 -- may also need to be changed.
    self:createNewDungeon()
    self.cofloater = Floater(50,25,4)
    self.musicPlayer = MonsterPlayer(self)
    self.dungeonLevel = 0
    self.requestNewLevel = false
    self.playerRoom = 1
    Bus:subscribe(self, self.createNewLevel, "createNewLevel")
    Bus:subscribe(self, self.darkness, "darkness")
end

And:

function GameRunner:darkness()
    Tile:illuminationLimit(3)
end

Now we need to place a Darkness somewhere in the Learning Level. Let’s put it just two to the right of the Player for now.

function GameRunner:placeDarkness()
    local r1 = self.rooms[1]
    local tile = r1:centerTile()
    local darkTile = tile:getNeighbor(vec2(2,0)(
    local dark = Darkness(darkTile)
end

Copying Decor shows me that I need to pass the tile to Darkness. Makes sense.

function Darkness:init(tile)
    tile:moveObject(self)
end

function Darkness:actionWithPlayer(aPlayer)
    Bus:publish("darkness")
end

Now to place it. I think I’ll just place it in the main dungeon, so I paste self:placeDarkness() into createLevel. I can hardly believe this will work but I think I’ve done all I can.

darkness descends

There ya go. Maybe we should make the Darkness visible. Easily done:

function Darkness:init(tile)
    tile:moveObject(self)
    self.sprite = asset.button_up_off
end

function Darkness:actionWithPlayer(aPlayer)
    Bus:publish("darkness")
    self.sprite = asset.button_down_on
end

function Darkness:draw(tiny, center)
    if tiny then return end
    pushMatrix()
    translate(center.x, center.y)
    sprite(self.sprite,0,0, 50,50)
    popMatrix()
end

button changes, barely visible

Wow. It does change, but it’s sure hard to see. Here are some blown-up views:

button closeup, up

button closeup, down

We may want to enhance that drawing a bit, but our mission here is accomplished.

Commit: New Darkness object placed in startup room, shows floor button, cuts lighting to 3 cells.

Nearly lunch time. Let’s sum up.

Summary

An off-hand small suggestion from Bruce, to use publish / subscribe for announcing, led to a serious consideration of an architecture more focused on the p/s model, less on direct connections, especially upward in the objects.

Brief consideration made it seem not very difficult, requiring just addition of a Bus:subscribe to the object’s init. It looked like making a publish return results would be pretty easy, though we can see that there’s an issue of whether you expect only one or several: either is possible.

There’s an issue of what happens if you issue a request expecting one responder and there are several, but the Bus won’t care, it’ll just return what it gets.

We mused about efficiency and ran a moderately realistic experiment, and it looks like it may add 25% to the cost of a call, more if the call itself is trivial, perhaps as much as double. The raw added time is about one microsecond per call compared to direct access.

That seems feasible for most things, and we can always use a direct call if need be, at the cost of an additional pointer that might otherwise not be present.

And we tried a simple Darkness object, which, in all honesty, isn’t as much a full application of the concept as a sensible implementation of a Darkness-creating object. A full application would be to use the p/s approach to actually get something back from the publish. We’ve not found a place for that yet: most likely we’ll first use it to begin to unwind upward connections in the overall design.

Summing up, though, an interesting morning, leading from reflecting on a small idea writ large, a nice little experiment, and a really irritating Darkness button that, once you step on it, makes the rest of your day dark.

Tomorrow, maybe we’ll shed some light. See you then!


D2.zip