Dungeon 305
Time to pick something to do. This always surprises me. Late update: THIS IS NOT GOOD. WE HAVE A MAJOR SHIPPED DEFECT!
There are now a lot more yellow sticky notes, my local equivalent to 3x5 cards, on my desk:
Adding to my confusion, something strange has happened to my morning banananana:
I shall try to make do. One “card” catches my eye:
Leftover people in Learning Level??
A few days back, when testing the Learning Level, I noticed what seemed to be copies of the Princess and the Horn Girl showing up on the screen. There may have been a Lever as well. I hypothesized at the time that something had not been cleared during the transition to Learning Level. Since it has a very separate build than the regular levels, it might be a problem in initialization.
We’ll start the morning, what’s left of it, by looking for that. First, I’ll make an attempt at reproducing the problem in the game.
I don’t see any duplicates yet, but when I do the ?? button near the Level’s sample health powerup, I get another message, about a “mysterious chest”. Moving around and repeating the ??, I determine that the game thinks there is a chest a couple of tiles away from the powerup. After a bit of wandering, I see another Princess, a Horn Girl, a Lever, and a Pot come onto the screen:
Fiddling about, I find that these things are not noticed when I walk into them, but that when I do the ?? query, they do answer. The Horn Girl even starts telling me about her quest.
Let’s explore some code. In particular, if we look at how the ?? works, we should get a clue as to what’s going on. Then we’ll see if we can write a test showing the problem … and one way or another, we’ll fix the defect.
A quick scan tells me that the ?? button performs a command, “requestinfo”, on the player. I note the lower case “i”, which is not my usual style. Anyway:
function Player:requestinfo()
Bus:publish("requestinfo", self:currentTile())
end
And we find that Dungeon subscribes to that query:
function Dungeon:requestinfo(anyTile)
for i,neighbor in ipairs(self:neighbors(anyTile)) do
neighbor:doQueries()
end
end
Where do those neighbors come from? And what are they? They are tiles …
function Dungeon:neighbors(tile)
local tPos = tile:pos()
local offsets = self:neighborOffsets()
return ar.map(offsets, function(offset) return self:getTile(offset + tPos) end)
end
I want to know more. I do suspect that I could write a test to cause this, but I’m caught up in the chase.
This is delving down to the Tiles object.
Let’s look instead at doQueries
.
function Tile:doQueries()
for k,obj in pairs(self:getContents()) do
if obj.query then
obj:query()
end
end
end
Hm. getContents
:
function Tile:getContents()
return Bus:query("getTileContents", self)
end
Ah, hot on the trail now …
function DungeonContentsCollection:getTileContents(desiredTile)
local result = {}
for object,tile in pairs(self.contentMap) do
if tile == desiredTile then
table.insert(result,object)
end
end
return result
end
How are tiles shown to be equal? I bet I know … but I don’t. They are equal if identical. I was betting they just compared their coordinates.
Is it possible that over two runs of the game, a tile could be created and garbage collected and reused? Possible … but these tiles are showing in the same arrangement … which I guess could still happen, as they are always created the same way. But it’s more likely that the problem is in my code, not some weird quirk of garbage collection.
It’d be really fun if it were, though.
We do not assume, here chez Ron, that weird things in our programs are due to bugs or oddities in the environment. We assume that they are due to defects in our code. We’ll keep the weird possibility in mind, but more likely we’ve failed to clear the DungeonContents.
We do not clear it. We create new ones, here:
function GameRunner:dcPrepare()
TileLock = false
DungeonContentsCollection()
MonsterPlayer(self)
Announcer:clearCache()
end
That does this:
function DungeonContentsCollection:init()
self.contentMap = {}
self:dirty()
Bus:subscribe(self, "drawDungeonContents")
Bus:subscribe(self, "getTileContents")
Bus:subscribe(self, "moveObjectToMe")
Bus:subscribe(self, "moveObjectToTile")
Bus:subscribe(self, "removeObject")
Bus:subscribe(self, "tileContaining")
end
At this point I wonder: how does it hang around, we aren’t storing it as a global. The answer is a bit spooky: the Bus is holding on to it.
And that means that the Bus is holding on to any prior DungeonContents collection that might be around, unless someone were to clear it, replace it, or unsubscribe the prior DungeonContentsCollection.
I quickly verify that building the LearningLevel does call dcPrepare
, just in case something else weird is going on.
Let’s try to write a test for this. Where shall we put it? I find a call to defineDungeonBuilder
in a test suite. I also find a very tiny test suite for DungeonBuilder:
function testDungeonBuilder()
CodeaUnit.detailed = false
_:describe("DungeonBuilder", function()
_:before(function()
_bus = Bus
Bus = EventBus()
end)
_:after(function()
Bus = _bus
end)
_:test("creation", function()
local runner = GameRunner()
_:expect(runner.builder).is(nil)
local builder = runner:defineDungeonBuilder()
_:expect(builder, "no builder").isnt(nil)
_:expect(builder.RUNNER, "no runner").is(runner)
_:expect(builder.dungeon, "no dungeon").isnt(nil)
end)
end)
end
We should be able to work from here. What we want to do is to do a dcPrepare
, put something into the dungeon, do another dcPrepare
and make sure it’s gone.
Let’s try.
_:test("dcPrepare removes old dungeon contents", function()
local runner = GameRunner()
local builder = runner:defineDungeonBuilder()
builder:dcPrepare()
local object = "object"
local tile = "tile"
Bus:publish("moveObjectToTile", object, tile)
local contents = Bus:query("getTileContents", tile)
_:expect(contents).has(object)
end)
I’m not sure I can be that cavalier with the object and tile, but I rather expect this to work, and it does. We then extend:
_:test("dcPrepare removes old dungeon contents", function()
local runner = GameRunner()
local builder = runner:defineDungeonBuilder()
builder:dcPrepare()
local object = "object"
local tile = "tile"
Bus:publish("moveObjectToTile", object, tile)
local contents = Bus:query("getTileContents", tile)
_:expect(contents).has(object)
builder:dcPrepare()
contents = Bus:query("getTileContents", tile)
_:expect(contents, "it's still there!").hasnt(object)
end)
Note the hasnt
in the second expect
. This should fail, and it does:
2: dcPrepare removes old dungeon contents it's still there! -- Actual: table: 0x28673c480, Expected: object
It is tempting to try to make this work by creating a new Bus, but, unless I miss my guess, we have subscriptions that we need and cannot safely clear. Replacing the Bus is a big deal. For the same reason, we dare not clear the Bus. What we want is for the current DungeonContents collection to unsubscribeAll, removing itself from subsequent attention.
Remind me to discuss that this is the first really weird thing that has happened with the relatively new approach to using the Bus for queries and such.
So we want the DCC to subscribe to one more message, and to implement it.
function DungeonContentsCollection:init()
self.contentMap = {}
self:dirty()
Bus:subscribe(self, "drawDungeonContents")
Bus:subscribe(self, "getTileContents")
Bus:subscribe(self, "moveObjectToMe")
Bus:subscribe(self, "moveObjectToTile")
Bus:subscribe(self, "removeObject")
Bus:subscribe(self, "tileContaining")
Bus:subscribe(self, "dccDelete")
end
function DungeonContentsCollection:dccDelete()
Bus:unsubscribeAll(self)
end
Now, if we publish the dccDelete
message in our test, the test should run.
_:test("dcPrepare removes old dungeon contents", function()
local runner = GameRunner()
local builder = runner:defineDungeonBuilder()
builder:dcPrepare()
local object = "object"
local tile = "tile"
Bus:publish("moveObjectToTile", object, tile)
local contents = Bus:query("getTileContents", tile)
_:expect(contents).has(object)
Bus:publish("dccDelete")
builder:dcPrepare()
contents = Bus:query("getTileContents", tile)
_:expect(contents, "it's still there!").hasnt(object)
end)
Test runs. Sweet. We could put this into dcPrepare
, but the truth is, whenever we create a new DCC we should unsubscribe the old one. So remove our line from the test:
_:test("dcPrepare removes old dungeon contents", function()
local runner = GameRunner()
local builder = runner:defineDungeonBuilder()
builder:dcPrepare()
local object = "object"
local tile = "tile"
Bus:publish("moveObjectToTile", object, tile)
local contents = Bus:query("getTileContents", tile)
_:expect(contents).has(object)
builder:dcPrepare()
contents = Bus:query("getTileContents", tile)
_:expect(contents, "it's still there!").hasnt(object)
end)
And put it here:
function DungeonContentsCollection:init()
Bus:publish("dccDelete") -- remove any old contents
self.contentMap = {}
self:dirty()
Bus:subscribe(self, "drawDungeonContents")
Bus:subscribe(self, "getTileContents")
Bus:subscribe(self, "moveObjectToMe")
Bus:subscribe(self, "moveObjectToTile")
Bus:subscribe(self, "removeObject")
Bus:subscribe(self, "tileContaining")
Bus:subscribe(self, "dccDelete")
end
I expect the test to continue to run. It does. Commit: Fix problem where old objects remain in learning level.
Let’s reflect. I am a bit concerned that I don’t understand everything here.
Reflection
One small thing concerns me. Why didn’t I ever see these ghost contents when I moved from level to level? Was it just that I didn’t do very much of that, and so didn’t run across them? That’s probable, I rarely ever play even to the second level, much less play through it.
Nonetheless, I think I’d like to try it. I’ll turn off that fix and test in game. I am pleased to be able to duplicate the problem. Sweet, now I’m sure we got it. Revert out my patch.
More seriously …
What we have just encountered seems to me to be a fairly serious issue with our Bus pub-sub approach.
The Bus holds onto its subscribers. As we’ve seen here, it can hold on to things that aren’t even supposed to be in the game any more. We’ve fixed that for the DungeonContentsCollection, but there are plenty of other cases of Bus:subscribe. Let’s look at all of them:
- GameRunner
- Bus:subscribe(self, “createNewLevel”), Bus:subscribe(self, “darkness”)
- Dungeon
- Bus:subscribe(self, “requestinfo”)
- Button
- Bus:subscribe(self, “touchBegan”)
- Monster
- if self:name() == “Ankle Biter” then
Bus:subscribe(self, “dullSharpness”)
end - Player
- Bus:subscribe(self, “spawnPathfinder”)
Bus:subscribe(self, “curePoison”)
Bus:subscribe(self, “addPoints”) - Provider
- Bus:subscribe(self, “addTextToCrawl”)
Bus:subscribe(self, “addItemsToCrawl”) - Spikes
- Bus:subscribe(self, “lever”)
- Inventory
- Bus:subscribe(Inventory, “touchBegan”)
- DungeonContentsCollection
- Bus:subscribe(self, “drawDungeonContents”)
Bus:subscribe(self, “getTileContents”)
Bus:subscribe(self, “moveObjectToMe”)
Bus:subscribe(self, “moveObjectToTile”)
Bus:subscribe(self, “removeObject”)
Bus:subscribe(self, “tileContaining”)
Bus:subscribe(self, “dccDelete”) - NPC
- Bus:subscribe(self, “catPersuasion”)
My first reaction is: This is not good.
- We “clone” the player when we start a new level. I’m not entirely sure why we do that, but there is a new player instance on each level.
- If there is an AnkleBiter on any level, it can probably appear as a ghost in subsequent levels.
- Depending on when we create them, there might be duplicate buttons responding to touches. (I’m not sure whether they could all capture the touch or not.)
This is not good. The Bus prevents abandoned objects from being garbage-collected, which can result in ghostly behavior, and not the kind we want. We can quickly “resolve” the problem by creating a new Bus … but the new Bus would lose subscriptions for objects that were not garbage, such as the GameRunner and Dungeon. The Runner lasts for the entire duration of the game; there’s a new Dungeon for each level.
A “fix” might be to create a new Bus somewhere in DungeonBuilder and have GameRunner resubscribe to the Bus. But that’s a risky fix. We could nail it down with a test, but it’s still just working more or less by accident.
Probably even more mystical is The Curious Case of the Dungeon Collection. That object is only protected from garbage collection because it is subscribed to events, so that it is held on to only by the Bus.
Deep in the Bag of Tricks
Lua has a thing called “weak tables”. These are tables that hold onto their contents, well, weakly. That is, if an object would otherwise be collected, the weak table does not object. The object is collected and the table updated magically to no longer have the object.
This is so deep in the bag of tricks that I’ve never even tried it. However … if we were to make the Bus’s tables weak, it would no longer hold onto lost objects, and some of these concerns would go away.
Except. Garbage collection happens randomly, when the system wants to do it, although you can request a collection if you wish. That means that, for a while, some of the weakly-held objects in a “WeakBus” might be logically dead but physically uncollected, and they might be sent messages and they might take actions based on those messages.
This is not good.
We can make it work. All we have to do, really, is to create a new Bus for every level, and that’s probably already more than just a good idea: I think that when you’re in the Segundo Level, there are two Dungeons running. THIS IS NOT GOOD.
But we can make it work by creating a Bus for every new level and either resubscribing the GameRunner, or causing the GameRunner not to subscribe … until someone, with the best of will, subscribes it to something useful. So, no, we have to make it resubscribe.
We could do this: Suppose that if you subscribe to anything, you are automatically subscribed to some message that tells you to unsubscribe … or, better yet, suppose we have a Bus message that unsubscribes everyone except, perhaps, a protected object:
function GameRunner:refreshBus()
Bus:publish("unsubscribeAllButMe", self)
end
Well, if you knew to do that, you’d also know to replace the Bus and resubscribe. But some kind of bus-clearing message might still be desirable.
Fact is, I don’t know. I don’t even know whether there is a discernible defect in there right now. I do have one useful idea, and that is to build a Making tool that will dump the Bus. Let’s do that.
In Main:
parameter.action("Dump Bus", function()
if Bus then Bus:dump() end
end
I am somewhat surprised, when I press this, to see output. Apparently we have the function already. Sure enough:
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
The output is pretty messy, since print
double spaces in Codea.
Let’s clean it up just a tad:
function EventBus:dump()
print("Bus dump", self)
for event, subs in pairs(self.events) do
local entry = "Event: "..event
for listener, method in pairs(subs) do
entry = entry.."\n "..tostring(listener)
end
print(entry)
end
end
That’s better but still messy. I have at least two issues. First, I’d like to see the events in alphabetical order. Second, some tables do not have a __tostring
and just print as table. I’d like to know what they are. That’ll be easy to change. Let’s do the alpha order first.
function EventBus:dump()
print("Bus dump", self)
local events = {}
for event, _ignored in pairs(self.events) do
table.insert(events, event)
end
table.sort(events)
for _i, event in ipairs(events) do
local subs = self.events[event]
local entry = "Event: "..event
for listener, method in pairs(subs) do
entry = entry.."\n "..tostring(listener)
end
print(entry)
end
end
I just fetch the events, sort them, use them. Now they display in a standard order:
Now by quickly searching for the event names I can add __tostring
as needed.
I run the game, dump the bus, go down to another level and dump again, with this result:
Player:10: attempt to index a nil value
stack traceback:
Player:10: in function <Player:9>
[C]: in function 'tostring'
EventBus:224: in method 'dump'
Main:44: in function <Main:43>
I think this represents another player. Anyway:
function Player:__tostring()
return string.format("Player (%d,%d)", self:currentTile():pos().x,self:currentTile():pos().y)
end
Clearly we have no tile. Fix, do again.
The first report is this:
Bus dump EventBus
Event: addItemsToCrawl
Provider
Event: addPoints
Player (9,14)
Event: addTextToCrawl
Provider
Event: catPersuasion
NPC
Event: createNewLevel
GameRunner
Event: curePoison
Player (9,14)
Event: darkness
GameRunner
Event: dccDelete
DungeonContentsColl
Event: drawDungeonContents
DungeonContentsColl
Event: getTileContents
DungeonContentsColl
Event: lever
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Event: moveObjectToMe
DungeonContentsColl
Event: moveObjectToTile
DungeonContentsColl
Event: removeObject
DungeonContentsColl
Event: requestinfo
Dungeon
Event: spawnPathfinder
Player (9,14)
Event: tileContaining
DungeonContentsColl
Event: touchBegan
Button requestinfo
Button history
Button down
Button left
Button up
table: 0x28699cac0
Button learn
Button right
The second is this:
Bus dump EventBus
Event: addItemsToCrawl
Provider
Event: addPoints
Player (?,?)
Player (72,50)
Event: addTextToCrawl
Provider
Event: catPersuasion
NPC
NPC
Event: createNewLevel
GameRunner
Event: curePoison
Player (?,?)
Player (72,50)
Event: darkness
GameRunner
Event: dccDelete
DungeonContentsColl
Event: drawDungeonContents
DungeonContentsColl
Event: getTileContents
DungeonContentsColl
Event: lever
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Event: moveObjectToMe
DungeonContentsColl
Event: moveObjectToTile
DungeonContentsColl
Event: removeObject
DungeonContentsColl
Event: requestinfo
Dungeon
Dungeon
Event: spawnPathfinder
Player (?,?)
Player (72,50)
Event: tileContaining
DungeonContentsColl
Event: touchBegan
Button right
Button learn
Button history
Button down
Button requestinfo
Button down
Button up
Button right
Button left
Button learn
Button requestinfo
Button left
table: 0x28699cac0
Button up
Button history
Sure enough, we see two players, one at an unknown location, two NPCs, plus two Dungeons, duplicate buttons, and what is surely way too many Spikes.
Our tiny enhancement to our Making tools has confirmed what we already knew: the Bus, as it stands, is a Big Problem, because it is holding on to things that the game considers to be dead and gone.
This is a major undiscovered defect in the system.
There is a quick fix. We can find a spot in GameRunner to instantiate a new EventBus, and resubscribe GameRunner after doing so. I reckon we could do it right here:
function GameRunner:dcPrepare()
TileLock = false
DungeonContentsCollection()
MonsterPlayer(self)
Announcer:clearCache()
end
In fact, let’s do that.
function GameRunner:dcPrepare()
Bus = EventBus()
self:doSubscriptions()
TileLock = false
DungeonContentsCollection()
MonsterPlayer(self)
Announcer:clearCache()
end
function GameRunner:doSubscriptions()
Bus:subscribe(self, "createNewLevel")
Bus:subscribe(self, "darkness")
end
(And I’m calling that from init
now.)
That should do it. But we have no decent test for this, so I must play again.
Wow.I’ll spare you the printout, but there was no change. That surprises me.
Did we not do the dcPrepare? Yes, that’s right. We didn’t. We use the dcPrepare
in DungeonBuilder, not in GameRunner. Is that even called? Oh, right, in hex level. Remind me to fire whoever thought of that.
Fix DungeonBuilder somehow. We do know the runner, so we can certainly do the same thing in our dcPrepare
. Let’s try that …
function DungeonBuilder:dcPrepare()
Bus = EventBus()
self.RUNNER:doSubscriptions()
TileLock = false
self:createTiles()
self:clearLevel()
DungeonContentsCollection()
MonsterPlayer(self.RUNNER)
Announcer:clearCache()
end
Now I need to do this tedious testing again. I need an automated test badly. Remind me to discuss that in Summary.
Test in game.
First dump:
Bus dump EventBus
Event: addPoints
Player (9,20)
Event: addTextToCrawl
Event: catPersuasion
NPC
Event: createNewLevel
GameRunner
Event: curePoison
Player (9,20)
Event: darkness
GameRunner
Event: dccDelete
DungeonContentsColl
Event: drawDungeonContents
DungeonContentsColl
Event: getTileContents
DungeonContentsColl
Event: lever
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Event: moveObjectToMe
DungeonContentsColl
Event: moveObjectToTile
DungeonContentsColl
Event: removeObject
DungeonContentsColl
Event: spawnPathfinder
Player (9,20)
Event: tileContaining
DungeonContentsColl
Event: touchBegan
Button right
Button learn
Button down
Button left
Button history
table: 0x2824de180
Button up
Button requestinfo
Second dump:
Bus dump EventBus
Event: addPoints
Player (27,17)
Event: addTextToCrawl
Event: catPersuasion
NPC
Event: createNewLevel
GameRunner
Event: curePoison
Player (27,17)
Event: darkness
GameRunner
Event: dccDelete
DungeonContentsColl
Event: drawDungeonContents
DungeonContentsColl
Event: getTileContents
DungeonContentsColl
Event: lever
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Spikes
Event: moveObjectToMe
DungeonContentsColl
Event: moveObjectToTile
DungeonContentsColl
Event: removeObject
DungeonContentsColl
Event: spawnPathfinder
Player (27,17)
Event: tileContaining
DungeonContentsColl
Event: touchBegan
Button down
Button requestinfo
Button history
Button learn
Button up
Button right
Button left
Ah. That looks good. No duplicate entries. Commit: Fix released flaw consisting of extra objects persisting between levels. Possible ghosts on screen. Add some tostrings.
Enough for the day, I’ve been here for three hours and haven’t even finished my chai. Let’s sum up.
Summary
Much of the time this morning was spent in the game, trying to duplicate the leftover objects, and then later, running two levels to print and compare the Bus dumps.
That was time wasted, time that could have been spent figuring out what this defect tells us about the idea of the Bus. I just couldn’t bring myself to write a test for it, even though it’s probably pretty easy, and I want to talk about that issue.
During last Tuesday night’s Friday Night Coding session, GeePaw Hill was talking about how absolutely reliable he is about writing tests, except when he is explicitly doing a spike to figure something out (and he surely writes tests then, sometimes, as needed). I stated, as did another zoom attendee who will be unnamed (Bryan Beecham), that I do not always write tests, even when I know at the time that I need them.
Now Hill is such a moral paragon that I would not for a moment doubt his word that he “always” writes tests, but here I am before you, telling you on the one hand that I go faster with tests, and then by damn demonstrating right here before the cat and all, that often I do not do the thing that makes me go faster.
What can I say? I’m human. I forgive myself for doing unwise things. If I didn’t forgive myself, my morning blame list would be so long as to consume the entire day. I’ve done more than a couple of unwise things in my life.
What’s going on? Why do I do this to myself? What could I do to improve?
I could resolve to do better. File that with my resolution never to eat potato chips in the evening, my resolution to exercise every day, and my resolution to use MySql to store my growing resolution list. (Meta-resolution, cool!)
Resolving to do better sometimes helps a little. I did skip the chips one evening, I specifically recall the time.
One way to extinguish bad behavior is to replace it. Eat a carrot rather than light up a Lucky. (Do they still make Lucky Strike?) But am I trying to extinguish game play? I think not, but if so, I could put a timer into the game to blow me out of it after a short interval. That would teach me.
Maybe I could develop the habit of getting up to think before I dive into game play to find a problem. That would have the desirable side effect of putting a few steps on my pedometer. (Don’t you still have your Captain Midnight pedometer?)
What is the real issue here? Why don’t I test? I think it is because my model of the code, and the code’s behavior is in the front of my mind. I’m thinking about what happens in the code, where the bug might be, what to change to fix it … not to test those things. Probably I think of tests more as showing that the bug exists. Thinking of them as examples, or as discovery tools, or something else like that might help.
I did resolve to do Bus:dump. I didn’t remember that I had it, and I was entirely ready to write it. Why couldn’t I turn that resolution into writing a test … an example … showing that the problem occurred?
I don’t know. I don’t see what to do other than try to do better. Advice welcome, hit me up.
But this isn’t the worst thing. The worst thing is that the defect existed and was a pretty obvious effect of the Bus-oriented design. OK, not totally obvious, but I can state it in a sentence:
Bus subscribers are held onto after they would otherwise be collected, and will therefore receive messages and take action when logically dead and gone.
OK, my sentence has an “and” in it, so sue me. Point is, the bug is pretty obvious once you think of it.
Worse still, unless it is handled very carefully, the Bus can either hold onto objects that should be dropped, or lose subscriptions to objects we wish to preserve over its destruction and reconstitution. It’s conceptually simple but has some effects that are not obvious.
I’m very reluctant to get rid of it. I like the way the Bus allows objects to publish information and even ask questions, without knowing who’s going to deal with that information or provide answers.
And please note: This defect is not a function of the use of Bus to do queries. It holds on equally to all subscribers. So much for you unbelievers in using pub-sub to return results. 😀
But whether I like it or not, the Bus has shown itself to have properties more subtle than my simple understanding encompassed, and to be the potential source of problems. While making it use weak tables might help, that’s pretty deep in the bag, and I’m not sure whether it might have other issues.
One thing comes to mind: If the Bus subscribers didn’t include GameRunner, we could—and should—recreate it on each new level with impunity. It’s still subtle, but it would be safer in the current situation.
I must think. This tool is sharper than I thought. I threw away our mandoline rather than one day carve up my fingers. Should I throw the Bus away lest it carve up my program?
I can hardly wait to find out what I do.
Perhaps even more significant: this is at least the third defect that I have released to production in only about 300 releases. This is three too many. What am I to learn from this? I can’t fire myself: I haven’t the authority.