Somewhat Chaotic, but Good
I have no idea what we might do this morning. I’m sure I’ll have at least one notion quite soon.
I would like to improve the graphics, but unfortunately I haven’t yet found the tile set that I want to use. I’m sure it’s in my files somewhere, but while we could do a bit, I think it’ll be more fun with the right tiles.
One good place to start is often near where we left off last time. Ah, now an idea comes to me. Yesterday I built a simple hierarchy for the dungeon content:
Content
--DrawableContent
----Button
----ReceivableContent
------SecretKey
I say “I”, not “we”, generally speaking, when what I’m doing is particularly iffy, and I don’t want you thinking you’re to blame. Generally, I say “we” when things are going reasonably, because it feels friendly and collegial to me, and I hope it helps you feel like you’re a part of things. If you’re out there at all … but I digress.
There are issues with this scheme. I’ll list a couple:
-
I’m inheriting some concrete methods, such as where Button inherits Draw from DrawableContent. This is generally considered to be an inferior idea, as it tends to make things rigid. Not everyone agrees with that concern, and as my brother Hill says, the code works for me, I don’t work for the code, so if it works and isn’t nasty I’ll do it.
-
I think there are classifications that we can be pretty sure we’ll need, but that won’t fit nicely into the hierarchy. What about a floor tile? It will be drawable, certainly, but unlike the button, it won’t interact with the player. Does that imply that there is another level in there, InteractingDrawableContent?
Net net net, I think I don’t like this hierarchy. Let’s flatten our objects back out, for now, making them all inherit from the abstract Content class, and make them all do their own overrides. That will be safe enough, because Python and PyCharm will make sure we implement all the necessaries, and it’ll frankly be simpler, at least for now.
Yes, I know I just did it yesterday and now I’m tearing it out. I promise we won’t put it back tomorrow, although I do think we might possibly benefit from using the “mixin” notion. We’ll burn that bridge when we come to it.
Here goes. I’ll just change all those classes to inherit from Content, not from each other.
Whoa! Doing that all at once broke some tests. Let’s proceed a bit more judiciously. At least some of the errors referred to ReceivableContent and its lack of an init and draw. Providing those pains me:
class ReceivableContent(Content):
error_texture = ':resources:images/items/star.png'
def __init__(self, name, resource=None):
self.name = name
try:
self.shape = arcade.load_texture(resource)
except:
self.name = 'invalid resource: ' + name
self.shape = arcade.load_texture(self.error_texture)
def draw(self, cx, cy, size):
texture = self.shape
scale = size/128
arcade.draw_texture_rect(
texture,
arcade.XYWH(cx, cy, texture.width, texture.height).scale(scale)
)
def interact_with_player(self, dungeon):
dungeon.receive_item(self)
dungeon.announce(f'You have received {self.name}!')
def placed(self, dungeon):
pass
So much duplication! It burns! Well, once we have lots of duplication we can figure out better ways to get rid of it. Let’s do the Button class next. One issue here is that we have no tests for Button.
class Button(Content):
error_texture = ':resources:images/items/star.png'
def __init__(self, name, resource=None):
resource = ':resources:images/items/flagGreen1.png'
self.name = name
try:
self.shape = arcade.load_texture(resource)
except:
self.name = 'invalid resource: ' + name
self.shape = arcade.load_texture(self.error_texture)
def draw(self, cx, cy, size):
texture = self.shape
scale = size/128
arcade.draw_texture_rect(
texture,
arcade.XYWH(cx, cy, texture.width, texture.height).scale(scale)
)
def interact_with_player(self, dungeon):
dungeon.announce("Something happened!?!?")
dungeon.publish('button_pressed')
def placed(self, dungeon):
pass
As I was passing through, I noticed that when you step on the Button (flag) there is no indication that anything has happened. The key does appear but in general it might not be in view, so I added the scroll announcement “Something happened!?!?”. So far so good. But now there is another issue, which is that if we step on the Button again, it issues the message again, and of course publishes its message.
Since we’re green, let’s think a bit about what the Button should do and make it so.
It should init in the “up” state, and when stepped on go to the “down” state. We would expect that if you step on it again it would go to the “up” state again. There would be two messages, presumably, “button down” and “button up”.
And, if you want to get fiddly about it, this particular button is arguably a one-shot button. We probably want it to trigger once and never again. And, presumably we might have more than one such button in the dungeon and we’d like to be able to distinguish one from another. So, I’d guess, we would like to specify the up and down messages for a button, and whether it is one shot or many … this is getting messy, and very speculative. Oh, and I forgot to mention that we’ll want two pictures for the button, one up and one down.
No. Too much, too speculative. What I will do is change the SecretKey to issue the commentary when it decides to appear. We’ll generalize things later, if it seems appropriate. Right now, we just don’t know enough.
OK, I put this into SecretKey:
class SecretKey:
def placed(self, dungeon):
self.visible = False
def callback(event):
dungeon.announce('A key has appeared!')
self.visible = True
dungeon.subscribe('button_pressed', callback)
And guess what happens! Every time you step on the flag, you get that key has appeared message. Why? Because the key still exists and is still subscribed to the button’s publication. We need unsubscribe.
Should we divert to deal with this, or should we continue to undo the hierarchy?
Even if we had unsubscribe, which we don’t, it would be easy to forget to do it, with results like above, where things happen that should not. It seems clear that there will be long-lasting connections, such as a lever that opens and closes a door, but also one-shot connections like we have here between the button and the key.
Perhaps there should be a subscribe_once method on PubSub, such that when you get the message, your subscription will be cancelled. Let’s at least review PubSub to refresh our memory and enable thinking about it.
class PubSub:
def __init__(self):
self.subscriptions = dict()
def subscribe(self, event, callback):
try:
self.subscriptions[event].append(callback)
except KeyError:
self.subscriptions[event] = [callback]
def publish(self, event):
try:
subs = self.subscriptions[event]
for callback in subs:
callback(event)
except KeyError:
pass
I’m sure we’ll be back here soon, because we’re going to need parameters on our publications.
This implementation is interesting in that the PubSub doesn’t know what objects are sending or receiving the events. It just has functions to be called. We do have some tests for PubSub:
def test_first_usage(self):
called = False
def call_me(event):
nonlocal called
assert event == 'event'
called = True
pub_sub = PubSub()
pub_sub.subscribe('event', call_me)
pub_sub.publish('event')
assert called
def test_two_subscribers(self):
called_sub_1 = False
def called_1(event):
nonlocal called_sub_1
called_sub_1 = True
called_sub_2 = False
def called_2(event):
nonlocal called_sub_2
called_sub_2 = True
pub_sub = PubSub()
pub_sub.subscribe('event', called_1)
pub_sub.subscribe('event', called_2)
pub_sub.publish('event')
assert called_sub_2
assert called_sub_1
Let’s have a test for subscribe_once:
def test_two_subscribers_one_once(self):
called_sub_1 = False
def called_1(event):
nonlocal called_sub_1
called_sub_1 = True
called_sub_2 = False
def called_2(event):
nonlocal called_sub_2
called_sub_2 = True
pub_sub = PubSub()
pub_sub.subscribe_once('event', called_1)
pub_sub.subscribe('event', called_2)
pub_sub.publish('event')
assert called_sub_2
assert called_sub_1
called_sub_1 = False
called_sub_2 = False
pub_sub.publish('event')
assert called_sub_2
assert not called_sub_1
Let’s get the test to fail on the assertions by short-circuiting subscribe_once:
def subscribe_once(self, event, callback):
self.subscribe(event, callback)
This should fail on the final line of the test. And it does:
> assert not called_sub_1
E assert not True
My plan is to build a tiny object called a Subscription, containing the callback and a boolean indicating whether it’s a “once” or not.
class Subscription:
def __init__(self, callback, once: bool):
self.callback = callback
self.once = once
And we’ll do this:
class PubSub:
def __init__(self):
self.subscriptions = dict()
def subscribe(self, event, callback, once=False):
subscription = Subscription(callback, once)
try:
self.subscriptions[event].append(subscription)
except KeyError:
self.subscriptions[event] = [subscription]
def subscribe_once(self, event, callback):
self.subscribe(event, callback, once=True)
def publish(self, event):
try:
subscriptions = list(self.subscriptions[event])
for subscription in subscriptions:
subscription.callback(event)
if subscription.once:
self.subscriptions[event].remove(subscription)
except KeyError:
pass
We create a little Subscription for each subscribe, indicating whether it is a “once” or not. We loop over a copy of the subscriptions for a published event, so that if it is a “once”, we can remove it.
Writing that tells me that the name of the flag should be one_shot. We make it so throughout.
And in SecretKey:
class SecretKey:
def placed(self, dungeon):
self.visible = False
def callback(event):
dungeon.announce('A key has appeared!')
self.visible = True
dungeon.subscribe_once('button_pressed', callback)
Dungeon needs to forward the message, we make that so and sure enough we only get the message that a key has appeared once, as intended.
Let’s reflect.
Reflection
The morning has been a bit chaotic. We started unwinding the hierarchy of Content. Then I noticed that the messaging from the button and key were odd. I diverted away from hierarchy, tried making the Button say something, which sent out lots of messages. So I moved the message to the key and it still came out, because the key was still subscribed.
So I diverted again, this time enhancing the PubSub to support a subscribe_once method, because it seemed to me that we would often forget to unsubscribe in things that wanted just one shot. I am somewhat concerned about how to do an actual unsubscribe as things stand, because we have no key to look up in the subscriptions. We may need something more robust. If so, it would be good to discover that soon, before we get too many subscriptions set up and have to refactor them to some new scheme.
Chaos notwithstanding, we’ve done some good. PubSub is substantially stronger with the one shot added, and we’re on the way to unwinding the hierarchy of Content items. I’m sure that we’re going to get some duplication as we build more content, and I’m concerned that it’s going to get worse before it gets better, but I do feel that the hierarchy, while fun, was premature. We’ll have everyone depend on the abstract base class Content, which will ensure that we provide all the subclasses with the necessary methods, and we’ll see what duplication arises and how we might deal with it.
I see at least two options for that. We could produce some little objects or functions that we can plug in, or some classes that we can mix in as needed. Either way, I’m going to have to try some things to see what I like, and to learn the best way to do what we need. Well, if not best, then “OK”. There may be no “best” in this work.
I think we have reached a good stopping point, and when that happens, the thing to do is to stop. See you next time!