Dungeon 149
Let’s catch a bus and see what happens. Maybe it will take us to the WayDown.
One of the things that troubles me about the current design is that the coupling between Entities, Tiles, and GameRunner gets in the way of testing. The problem is that I have to set up a complicated combination of those objects, all too often, and in order to get GameRunner happy, we almost wind up creating an entire runnable game just to test one object.
One way to reduce coupling (sort of) is to use a publish-subscrbe interface, using what’s called an “Event Bus”. This is like an electrical bus, no not a battery-operated bus, oh never mind, you know what I mean.
Anyway, the EventBus will be a well-known object. Objects that do things that need to be known by other objects “publish” a message to the Bus saying what has been done. Any object that’s interested in that thing can “subscribe”, and when that thing is published, all the subscribers get the message.
I don’t recall ever using this pattern explicitly, or at least not in this century, so this will be a bit of an experiment and learning experience for me. I hope you’ll find it informative or entertaining as well.
As I generally try to do, we’ll start small and let this thing grow. However, it seems prudent to create the thing with TDD, which means we’ll be making some assumptions about how it should work. When we learn otherwise, the basics should be OK, because they are tested, and with any luck, the specifics will be easy to modify.
Let’s Get Started
I’ll create a new tab, EventBus, with a CodeaUnit core in it:
-- EventBus
-- RJ 20210414
function testEventBus()
CodeaUnit.detailed = false
_:describe("EventBus publish/subscribe", function()
_:test("hookup", function()
_:expect(EventBus).isnt(nil)
end)
end)
end
As expected the first run fails:
loading testEventBus()
Feature: EventBus publish/subscribe
1: hookup -- Actual: nil, Expected: nil
OK out of an abundance of interest in tiny steps, let’s make this test pass.
EventBus = class()
Amazing, the test passes. Let’s do a new test, just for fun, rather than extend the hookup one.
_:test("can subscribe to a message", function()
local eb = EventBus()
local listener = FakeListener()
eb:subscribe(listener, listener.happened, "it happened")
itHappened = false
eb:publish("it happened")
_:expect(itHappened).is(true)
end)
This is close to my best guess as to what we’ll want, except that I think that we may want publish
to get the identity of the sender, so that it can be passed on to the listener, so that they can engage in a conversation if they want to.
This will fail a few times and then work, if I have my way.
After fixing a typo:
2: can subscribe to a message -- EventBus:17: attempt to call a nil value (global 'FakeListener')
I’ll write FakeListener as incrementally as I can, just to show that I can really do pure TDD if I want to.
FakeListener = class()
2: can subscribe to a message -- EventBus:18: attempt to call a nil value (method 'subscribe')
function EventBus:subscribe(listener, method, event)
end
I did put in the calling sequence. Pretend the compiler cares, but it doesn’t. So I’ve already written more code than is required to pass the test. So sue me.
2: can subscribe to a message -- EventBus:20: attempt to call a nil value (method 'publish')
function EventBus:publish()
end
OK, are you happy now? I put in the minimum.
2: can subscribe to a message -- Actual: false, Expected: true
Now can I do some work?
We’ll need to do enough to send the input message to FakeListener and for it to set the flag. To play really pure TDD here, I’d just save the one listener, and its method:
function EventBus:subscribe(listener, method, event)
self.listener = listener
self.method = method
end
function EventBus:publish()
self.method(self.listener)
end
I think this will do the job. We’ve presumably been passed an actual method, i.e. function, so we call it. We have no test to demand other parameters.
2: can subscribe to a message -- EventBus:36: attempt to call a nil value (field '?')
Thats because my FakeListener doesn’t have a “happened” method yet.
function FakeListener:happened()
end
2: can subscribe to a message -- Actual: false, Expected: true
My happened
method is incomplete, not setting the flag.
function FakeListener:happened()
itHappened = true
end
Feature: EventBus publish/subscribe
2 Passed, 0 Ignored, 0 Failed
Now these steps are almost ludicrously tiny, but they meet the strict rule of TDD, “never write a line of code except to fix a broken test”. It’s probably good practice to follow this rule sometimes, because if we write tiny tests as well, it’s good practice breaking things down into tiny pieces. And that always pays off.
What was the “extra” cost of doing the work above, besides writing the words in the article, which doesn’t count? It was pressing the test button and making sure that the error that came out was the one I expected. So far, I haven’t had to change a line of code, it has been strictly addition. That’s about to change, however.
But not yet. This time I’ll enhance the test. Let’s think about how to do that, since there are several ways we could go. Among the things not done yet:
- More than one subscriber to an event, all get the message
- More than one event, non-subscribers don’t get the message.
I have a solution in mind for the EventBus. That’s often true, and always OK. TDD doesn’t require us to be ignorant or stupid, it just helps us go in small steps.
My rough solution idea is that for each event, there will be a collection of subscribers, and for each subscriber, the collection will tell us what method to call on that subscriber, as well as who to call.
Let’s do multiple subscribers. I think I’ll have them provide the same method, but we’re still going to have to save it, so we can call back. Let’s pretend that I don’t realize that.
I’ve talked myself into doing a new test. That’s often best, maybe usually best.
Part way through writing the test … I have this much:
_:test("two subscribers both get message", function()
local eb = EventBus()
local listener1 = FakeListener()
local listener2 = FakeListener()
eb:subscribe(listener, listener.happened, "it happened")
end)
I realize that for my convenience, I want the FakeListener to do its own subscribing. That will force me to make it a bit more public:
local Bus
_:before(function()
Bus = EventBus()
end)
And a mod to my first test. I remove the draft second one.
_:test("can subscribe to a message", function()
local listener = FakeListener()
itHappened = false
Bus:publish("it happened")
_:expect(itHappened).is(true)
end)
This fails, which I expected, but the failure surprised me just a bit:
2: can subscribe to a message -- EventBus:43: attempt to call a nil value (field 'method')
Since no one is subscribing, the bus crashes. I’m not concerned, that’s the problem we’re solving, just with a different hat on.
Enhance FakeListener to subscribe automatically. Real objects will likely do that as well.
function FakeListener:init()
Bus:subscribe(self, self.happened)
end
This time I didn’t pass in the name of the event. We all know it’s needed, but I left it out to see where TDD discovers it’s missing.
The test passes again. Back to two subscribers. I realize that true/false won’t help me. I want more information back. For now, let’s change the flag to a counter:
_:test("can subscribe to a message", function()
local listener = FakeListener()
itHappened = 0
Bus:publish("it happened")
_:expect(itHappened).is(1)
end)
2: can subscribe to a message -- Actual: true, Expected: 1
And …
function FakeListener:happened()
itHappened = itHappened + 1
end
Test should pass … and it does. Now then:
_:test("two subscribers each get message", function()
local lis1 = FakeListener()
local lis2 = FakeListener()
Bus:publish("it happened")
_:expect(itHappened).is(2)
end)
This should fail, still saying 1.
It doesn’t. I forgot to init itHappened
. Let’s init it in before
so we don’t mess up again. (We will wind up changing it soon, as well.)
_:before(function()
Bus = EventBus()
itHappened = 0
end)
_:test("can subscribe to a message", function()
local listener = FakeListener()
Bus:publish("it happened")
_:expect(itHappened).is(1)
end)
_:test("two subscribers each get message", function()
local lis1 = FakeListener()
local lis2 = FakeListener()
Bus:publish("it happened")
_:expect(itHappened).is(2)
end)
Should fail now.
3: two subscribers each get message -- Actual: 1, Expected: 2
Good. Now we can make a collection:
function EventBus:init()
self.subscribers = {}
end
function EventBus:subscribe(listener, method, event)
self.subscribers[listener] = method
end
function EventBus:publish()
for subscriber,method in pairs(self.subscribers) do
method(subscriber)
end
end
The test runs. Now, fact is, I’m sure this is good. But suppose for a moment that I wasn’t sure that the two different instances of FakeListener were each getting the call, but that somehow one of them got both calls. Let’s test for that by changing what they do in their happened
method.
_:before(function()
Bus = EventBus()
itHappened = {}
end)
function FakeListener:happened()
itHappened[self] = self
end
_:test("can subscribe to a message", function()
local listener = FakeListener()
Bus:publish("it happened")
_:expect(itHappened).has(listener)
end)
_:test("two subscribers each get message", function()
local lis1 = FakeListener()
local lis2 = FakeListener()
Bus:publish("it happened")
_:expect(#itHappened).has(lis1)
_:expect(#itHappened).has(lis2)
end)
The FakeListeners add themselves to the itHappened
collection, and we check in the tests to see if they are there.
Test finds a mistake:
3: two subscribers each get message -- codeaUnit:94: bad argument #1 to 'for iterator' (table expected, got number)
Ah, those # in the second test shouldn’t be there. I had been thinking I could just count the table and then changed my mind. Failed to fix the test. Let’s try now.
Yep, all good.
What next? Listeners want to get some parameters in their listener method. They want the event name, and the sender, so that they can talk with him. Furthermore, we want to be able to put information in the publish event. Let’s set the rule that the information packet is always a table, and the form of that table is part of the implicit agreement between the publisher’s “API”.
First, I’ll change the FakeListener to check the event name for being the one they subscribed to. To make that harder, we’ll enhance a test or write a new one. For now, we can do this:
function FakeListener:happened(event)
if event == "it happened" then
itHappened[self] = self
end
end
The tests should all fail. They do, with CodeaUnit’s useless error message on has
. But we knew what to expect.
Fix EventBus:
function EventBus:publish(event)
for subscriber,method in pairs(self.subscribers) do
method(subscriber, event)
end
end
Tests run again. But we want to pass the publishing object as well:
_:test("can subscribe to a message", function()
local listener = FakeListener()
Bus:publish("it happened", 7734)
_:expect(itHappened).has(listener)
end)
function FakeListener:happened(event, sender)
if event == "it happened" and sender == 7734 then
itHappened[self] = self
end
end
function EventBus:publish(event, sender)
for subscriber,method in pairs(self.subscribers) do
method(subscriber, event, sender)
end
end
Test runs. Now the information packet.
local Bus
local itHappened
local info
_:before(function()
Bus = EventBus()
itHappened = {}
info = { info=58008 }
end)
_:test("can subscribe to a message", function()
local listener = FakeListener()
Bus:publish("it happened", 7734, info)
_:expect(itHappened).has(listener)
end)
_:test("two subscribers each get message", function()
local lis1 = FakeListener()
local lis2 = FakeListener()
Bus:publish("it happened", 7734, info)
_:expect(itHappened).has(lis1)
_:expect(itHappened).has(lis2)
end)
Test should fail. No it didn’t and shouldn’t. We have to check in the FakeListener:
function FakeListener:happened(event, sender, info)
if event == "it happened" and sender == 7734 and info.info == 58008 then
itHappened[self] = self
end
end
This better fail. Yes. Fails because there is no table. Not the error I expected. Let’s fix that by defaulting info to an empty table. This is a bit tricky to do in tiny steps. I’m impatient. Just do it:
function EventBus:publish(event, sender, info)
for subscriber,method in pairs(self.subscribers) do
method(subscriber, event, sender, info or {})
end
end
OK, tests run. Let’s review EventBus as a whole:
EventBus = class()
function EventBus:init()
self.subscribers = {}
end
function EventBus:subscribe(listener, method, event)
self.subscribers[listener] = method
end
function EventBus:publish(event, sender, info)
for subscriber,method in pairs(self.subscribers) do
method(subscriber, event, sender, info or {})
end
end
We presently send all events to all subscribers, paying no attention to what they wanted. Let’s enhance the FakeListeners to subscribe to an event provided in the creation method, and then we’ll make one that listens to something else, to break our test.
We are enhancing the test at this point, not the EventBus, preparing a platform for another test.
function FakeListener:init(event)
Bus:subscribe(self, self.happened, event)
self.event = event
end
function FakeListener:happened(event, sender, info)
if event == self.event and sender == 7734 and info.info == 58008 then
itHappened[self] = self
end
end
Now the listeners can subscribe to different events. The current ones all want the same event:
_: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)
Should still work. It does.
Double check, change the first one:
_:test("can subscribe to a message", function()
local listener = FakeListener("it never happened")
Bus:publish("it happened", 7734, info)
_:expect(itHappened).has(listener)
end)
Should fail. It does. Put it back as it was. Passes.
Now we can test for more than one event.
_:test("can subscribe to different events", function()
local lis1 = FakeListener("it happened")
local lis2 = FakeListener("something else happened")
Bus:publish("it happened")
_:expect(itHappened, "lis1 didn't get message").has(lis1)
_:expect(itHappened, "lis2 received wrong message").hasnt(lis2)
end)
Run this. I get a failure but it surprises me:
4: can subscribe to different events lis1 didn't get message -- Actual: table: 0x291720880, Expected: table: 0x2917209c0
Oh, I need to pass in the other parms:
_: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)
OK this works now, because the FakeListeners ignore messages that they weren’t expecting. But what we don’t test is whether they received any message (we know they did, of course). The FakeListener is too good, ignoring things that it doesn’t like. Let’s change it not to check the event, which will force us to handle the events properly in EventBus:publish
.
function FakeListener:happened(event, sender, info)
if sender == 7734 and info.info == 58008 then
itHappened[self] = self
end
end
This will break some stuff.
4: can subscribe to different events lis2 received wrong message -- Actual: table: 0x28905a700, Expected: table: 0x2890581c0
Good enough. We can enhance EventBus now:
function EventBus:subscribe(listener, method, event)
local subscriptions = self:subscriptions(event)
subscriptions[listener] = method
end
function EventBus:publish(event, sender, info)
for subscriber,method in pairs(self:subscriptions(event)) do
method(subscriber, event, sender, info or {})
end
end
We want a list for each event. So:
function EventBus:init()
self.events = {}
end
function EventBus:subscriptions(event)
local subs = self.events[event]
if not subs then
self.events[event] = {}
end
return self.events[event]
end
I’m not loving that last method, but I’m trying to get to green. I think this does it.
And it does. I think I should commit. I could have committed any time when green, but I have been in the mode of “not until done” but that’s silly with an unused object. In fact we should always be able to commit whenever all the tests run.
Commit: Event bus handles multiple events.
I feel better now.
What next? We should be able to unsubscribe, though we are pretty deep into speculation with that. But the pattern calls for unsubscribe.
It occurs to me that there are two kinds of unsubscribing. Unsubscribing from one specific event, or from all events. Let’s do the “all” case:
_: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).hasnt(lis1)
end)
Last check should fail. Actually a bigger fail than I had my mouth set for:
5: can unsubscribe -- EventBus:49: attempt to call a nil value (method 'unsubscribeAll')
Right.
function EventBus:unsubscribeAll(object)
end
That should give me the error I expect;
5: can unsubscribe -- Actual: table: 0x29d756480, Expected: table: 0x29d7561c0
Let’s make that more self-documenting:
_: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)
5: can unsubscribe lis1 was unsubscribed, got message -- Actual: table: 0x281359dc0, Expected: table: 0x28135b080
Right. Now we’d best implement something.
function EventBus:unsubscribeAll(object)
for event,subs in pairs(self.events) do
subs[object] = nil
end
end
That should be all there is to it. Tests do pass.
Commit again: EventBus unsubscribe.
OK, let’s review the object again.
Review
EventBus = class()
function EventBus:init()
self.events = {}
end
function EventBus:subscribe(listener, method, event)
local subscriptions = self:subscriptions(event)
subscriptions[listener] = method
end
function EventBus:publish(event, sender, info)
for subscriber,method in pairs(self:subscriptions(event)) do
method(subscriber, event, sender, info or {})
end
end
function EventBus:subscriptions(event)
local subs = self.events[event]
if not subs then
self.events[event] = {}
end
return self.events[event]
end
function EventBus:unsubscribeAll(object)
for event,subs in pairs(self.events) do
subs[object] = nil
end
end
The subscriptions method could perhaps be neater. Let’s see what we can do. It’d be nice to avoid the double call of self.events[event]
.
function EventBus:subscriptions(event)
local subs = self.events[event]
if not subs then
subs = {}
self.events[event] = subs
end
return subs
end
That’s a bit better. Test. Tests run. Commit: small improvement to EventBus:subscriptions
method.
And I’ll take a break. Back soon.
Back
Back, all nice clean and with a raisin cinnamon bagel to much on.
Lets use the EventBus and see whether we like it. I propose to do access to the crawl via the bus, since that’s one of the ways people need to use GameRunner. Where and when should we create the bus? One candidate is that we’d do it in Main, where we create GameRunner. Another is to let GameRunner do it. Since we’re trying to reduce coupling, let’s have Main do it. We’ll do it after we run the tests. But we should improve the tests no save and restore the Bus, anyway.
function setup()
if CodeaUnit then
runCodeaUnitTests()
CodeaTestsVisible = true
end
local seed = math.random(1234567)
print("seed=",seed)
math.randomseed(seed)
showKeyboard()
TileLock = false
Bus = EventBus()
Runner = GameRunner()
Runner:createLevel(12)
TileLock = true
if false then
sprite(xxx)
end
end
Are you wondering about that sprite call? That’s there to make it easy to browse sprites. When I touch the xxx, Codea hooks me up with the various sprite storage locations. It has no run-time effect, being ifed out.
Now the test, before I forget. Ah, no need, it has a local definition of Bus, so it won’t affect the other users. Great.
Now let’s look at how the crawl gets set up.
function GameRunner:init()
self.tileSize = 64
self.tileCountX = 85 -- if these change, zoomed-out scale
self.tileCountY = 64 -- may also need to be changed.
self.cofloater = Floater(self, 50,25,4)
self.musicPlayer = MonsterPlayer(self)
self:initializeSprites()
self.dungeonLevel = 0
self.requestNewLevel = false
self.playerRoom = 1
end
And there are a few GameRunner methods referring to cofloater
:
function GameRunner:addTextToCrawl(aString)
self.cofloater:addItem(CombatRound():display(aString))
end
function GameRunner:addToCrawl(array)
self.cofloater:addItems(array)
end
function GameRunner:createLevel(count)
self.dungeonLevel = self.dungeonLevel + 1
if self.dungeonLevel > 4 then self.dungeonLevel = 4 end
TileLock=false
self:createTiles()
self:clearLevel()
self:createRandomRooms(count)
self:connectRooms()
self:convertEdgesToWalls()
self:placePlayerInRoom1()
self:placeWayDown()
self:placeSpikes(5)
self:setupMonsters(6)
self.keys = self:createThings(Key,5)
self:createThings(Chest,5)
self:createLoots(10)
self:createDecor(30)
self:createButtons()
self.cofloater:runCrawl(self:initialCrawl(self.dungeonLevel))
self:startTimers()
self.playerCanMove = true
TileLock = true
end
function GameRunner:drawMessages()
pushMatrix()
self:scaleForLocalMap()
self.cofloater:draw()
popMatrix()
end
The first two, addTextToCrawl
and addToCrawl
are used by other objects. The runCrawl
just gets it started with an initial set of messages, and drawMessages
, I think, is just the crawl’s turn to draw.
Now what do we want? We want the objects that send addTextToCrawl
and addToCrawl
to publish a message on the Bus instead, and we want the Floater to be subscribed to those messages. Let’s do the text one first.
function Floater:init(runner, yOffsetStart, lineSize, lineCount)
self.runner = runner
self.provider = Provider("")
self.yOffsetStart = yOffsetStart
self.lineSize = lineSize
self.lineCount = lineCount
Bus:subscribe(self,self.addItem, "addTextToCrawl")
end
Now it seems to me that if I find all the addTextToCrawl
senders, I can change them like this:
function WayDown:actionWith(aPlayer)
if aPlayer:provideWayDownKey() then
self.runner:createNewLevel()
else
Bus:publish("addTextToCrawl", self, "You must have a key to go further!")
end
end
The Bus line there used to refer to self.runner:addTextToCrawl
. I forgot to paste a copy.
I think this ought to work. But no.
Floater:9: attempt to index a nil value (global 'Bus')
stack traceback:
Floater:9: in field 'init'
... false
end
setmetatable(c, mt)
return c
end:24: in global 'Floater'
GameRunner:10: in field 'init'
... false
end
setmetatable(c, mt)
return c
end:24: in global 'GameRunner'
Dungeon:13: in field '_before'
codeaUnit:44: in method 'test'
Dungeon:26: in local 'allTests'
codeaUnit:16: in method 'describe'
Dungeon:9: in function 'testDungeon'
[string "testDungeon()"]:1: in main chunk
codeaUnit:139: in field 'execute'
Tests:507: in function 'runCodeaUnitTests'
Main:6: in function 'setup'
Crash in the tests. That surprises me. Maybe my local confused things. Let’s do it more nearly right:
Still crashes. This is disconcerting. Let’s move the Bus creation up higher:
Putting it first in Main:setup
makes the problem go away.
When I try to go down the WayDown without a key, I get this:
Provider:42: unexpected item in Provider array no op
stack traceback:
[C]: in function 'assert'
Provider:42: in method 'getItem'
Floater:45: in method 'fetchMessage'
Floater:56: in method 'increment'
Floater:40: in method 'draw'
GameRunner:229: in method 'drawMessages'
GameRunner:186: in method 'draw'
Main:34: in function 'draw'
I think the Runner is wrapping the text and I forgot to do that.
Right:
function GameRunner:addTextToCrawl(aString)
self.cofloater:addItem(CombatRound():display(aString))
end
We’ll need to subscribe an equivalent method in Floater:
function Floater:addTextToCrawl(aString)
self:addItem(CombatRound():display(aString))
end
function Floater:init(runner, yOffsetStart, lineSize, lineCount)
self.runner = runner
self.provider = Provider("")
self.yOffsetStart = yOffsetStart
self.lineSize = lineSize
self.lineCount = lineCount
Bus:subscribe(self,self.addTextToCrawl, "addTextToCrawl")
end
That ought to do it. LOL: not quite:
We seem not to have something hooked up right.
function EventBus:publish(event, sender, info)
for subscriber,method in pairs(self:subscriptions(event)) do
method(subscriber, event, sender, info or {})
end
end
OK, first of all, we need to pass in an info table, and second, we should expect, in our subscription method, event, sender, and the info table.
I think we may be learning that we don’t quite like our calling sequence. Remember, we were just guessing, so it may be that we guessed less than perfectly.
For now let’s make it work.
function WayDown:actionWith(aPlayer)
if aPlayer:provideWayDownKey() then
self.runner:createNewLevel()
else
Bus:publish("addTextToCrawl", self, {text="You must have a key to go further!"})
end
end
But we have to set up Floater to expect what’s about to happen:
function Floater:addTextToCrawl(event, sender, info)
self:addItem(CombatRound():display(info.text or ""))
end
I’m starting to wish that I had chosen something that wasn’t so hard to test.
There we go! That’s working. Let’s see how we feel about the calling sequence:
Bus:publish("addTextToCrawl", self, {text="You must have a key to go further!"})
That’s a bit awkward, but for now I think I prefer the generality, until we see more uses.
Let’s see if WayDown uses the runner for anything else.
It does, right here in the same method:
function WayDown:actionWith(aPlayer)
if aPlayer:provideWayDownKey() then
self.runner:createNewLevel()
else
Bus:publish("addTextToCrawl", self, {text="You must have a key to go further!"})
end
end
In for a penny. Let’s publish this on the Bus also. But first, commit: WayDown publishes message via Bus
Now:
function WayDown:actionWith(aPlayer)
if aPlayer:provideWayDownKey() then
Bus:publish("createNewLevel")
else
Bus:publish("addTextToCrawl", self, {text="You must have a key to go further!"})
end
end
I realized that I don’t want to have to send in the self parm on a publish, if I’m not accepting callbacks. And no info is required.
Let’s check EventBus and its tests to be sure it can deal with this change:
function EventBus:publish(event, sender, info)
for subscriber,method in pairs(self:subscriptions(event)) do
method(subscriber, event, sender, info or {})
end
end
That’s going to work, but we don’t have a test for it. Make a note, we’re in the middle of a change to the business-side code. We need a subscribe in GameRunner:
function GameRunner:init()
self.tileSize = 64
self.tileCountX = 85 -- if these change, zoomed-out scale
self.tileCountY = 64 -- may also need to be changed.
self.cofloater = Floater(self, 50,25,4)
self.musicPlayer = MonsterPlayer(self)
self:initializeSprites()
self.dungeonLevel = 0
self.requestNewLevel = false
self.playerRoom = 1
Bus:subscribe("createNewLevel", self.createNewLevel, self)
end
No that’s wrong, it’s listener, method, event. I’ve made this mistake before. Keep that in mind.
function GameRunner:init()
self.tileSize = 64
self.tileCountX = 85 -- if these change, zoomed-out scale
self.tileCountY = 64 -- may also need to be changed.
self.cofloater = Floater(self, 50,25,4)
self.musicPlayer = MonsterPlayer(self)
self:initializeSprites()
self.dungeonLevel = 0
self.requestNewLevel = false
self.playerRoom = 1
Bus:subscribe(self, self.createNewLevel, "createNewLevel")
end
If this works, and I get a key, I should see the WayDown create a new level.
Works a treat. Now WayDown has no references to runner. Change its init and its callers:
function WayDown:init(tile)
tile:moveObject(self)
end
function GameRunner:placeWayDown()
local r1 = self.rooms[1]
local target = r1:centerTile()
local tile = self:getDungeon():farawayOpenRoomTile(target)
self.wayDown = WayDown(tile)
end
That’s the only reference. Commit: WayDown no longer needs to know GameRunner.
Should we strengthen the EventBus tests to make it more clear that publish’s second two args are optional?
Let’s improve the code instead:
function EventBus:publish(event, sender, info)
for subscriber,method in pairs(self:subscriptions(event)) do
method(subscriber, event, sender, info or {})
end
end
Let’s do this:
function EventBus:publish(event, optionalSender, optionalInfo)
for subscriber,method in pairs(self:subscriptions(event)) do
method(subscriber, event, optionalSender, optionalInfo or {})
end
end
I think we’ll call this a wrap. Let’s sum up:
Summary
The tests for EventBus seem pretty roBUSt. The object itself looks correspondingly solid to me. I do suspect that we’ll want a method to clear all subscriptions, but we do not have that at present.
We should perhaps be aware that once an object has subscribed to an event, it will be in the corresponding table, and therefore is not subject to garbage collection. We don’t have many throwaway objects, though the Pathfinder entity is ephermeral, so this is probably not a problem.
Lua does have the notion of a “weak” table, that holds objects, but if the object would otherwise be collected, the table lets it go. This is a bit deep under the covers, but it’s there if we need it.
The subscribe and publish operations are pretty easy to use, but the calling sequences are not obvious. We may have to revisit that. And we may possibly decide that the table for parameters is too complicated for its benefit. We’ll see: we do need some way to pass along arbitrary parameters.
It would have been nice to have been able to test the WayDown directly. Probably I could have. If so, I certainly should have, because wandering around looking for the WayDown consumed valuable time.
All in all, I’m happy with the EventBus so far.
See you next time!