Action at a Distance
Things happen. People have to know about them. What shall we do about this?
My Product Definition Personality chooses things to do based on some sense of risk or interest or perhaps just a feeling of a gap in the product and our knowledge of it. The technical term for this sense is “whim”.
Today PDP sees that we can wander in the dungeon, and display messages, and even receive things that we step on. PDP senses an issue relating to these simple behaviors: action at a distance. Let’s put it in terms of a game feature. PDP says:
Some places in the floor are traps. They can be active or inactive. When I.A. Dot steps on a trap, something bad will happen: typically she takes damage or is poisoned or the like. We’re concerned about that but it’s not our current concern.
There are other items in the dungeon that can enable or disable traps. One example: there is a big tempting switch in the room. When Dot touches it, it changes position, stepping through three or four states. One of those states disables the Floor Spikes, wherever they may be in the current level.
Let’s implement something like the Floor Switch / Floor Spikes, to show that things like that can be supported. Naturally, you’ll build the feature such that it will be fairly easy to devise new kinds of action at a distance.
Design Thinking
We can implement a simple demo, perhaps as simple as Dot stepping on a given square and another square’s contents change. The thing will be to implement it in a fashion that is simple enough to be built quickly, but that is clearly robust enough to handle the cases we can think of, perhaps with added capability that won’t be difficult.
We already have a game behavior that is a bit like this, and there are issues with it if we were to continue with it. It’s the logic that gives a ContentItem to Dot when she steps on it. Let’s review that code, since stepping on things is how Dot interacts with the game, at least so far.
When Dot moves, this sequence is executed:
class DungeonView:
def on_key_press(self, symbol: int, modifiers: int) -> bool | None:
...
if symbol == arcade.key.RIGHT:
self.dungeon.move_player_east()
...
class Dungeon:
# after some decoding ...
def place_player_at(self, cell):
self.player_cell = cell
for item in list(self.contents_at(cell)):
item.interact_with_player(self)
class ContentItem:
# player, here, is actually Dungeon.
def interact_with_player(self, player):
player.receive_item(self)
player.announce_received(self.name)
class Dungeon:
def receive_item(self, item):
self.player_inventory.append(item)
content = self.contents_at(self.player_cell)
if item in content:
content.remove(item)
def announce_received(self, item_name):
self.announcements.append(item_name)
So as this stands, the Dungeon sends each content item that is stepped on interact_with_player, and the items only have one behavior, which is to reply to that message with two messages, receive_item and announce_received.
One more fact worth noting. The Dungeon, at this point, has the item name in its announcements, and when the DungeonView updates, this code executes:
class DungeonView:
def on_update(self, delta_time: float):
def receive(item_name):
msg = f'You have received {item_name}!'
self.scroller.announce(msg)
self.dungeon.announce_via(receive)
self.scroller.update()
So, at this stage in our development, the Dungeon is mediating between items and player inventory, and its announcements only include item names, and the DungeonView assumes that all messages are about receiving something.
This clearly won’t do. Content items don’t just give things to Dot: we’ve just heard that they could cause some remote action at a distance, not involving Dot at all. And giving all that responsibility to the Dungeon will load more and more behavior into Dungeon, methods like receive_item and announce_received, which really have nothing to do with the Dungeon object at all.
What to do? I have an idea, because I’ve done this before. I don’t remember the details but the fundamental idea is that an object can Publish an Event, including an Event Name and some associated information-bearing objects, and other objects can Subscribe to Events by Name, and when that Event occurs, the object will be sent a message. [A miracle occurs right about here.]
Some issues come to mind right away. It is likely that a subscriber to one event might publish another event during processing of the one. Or it might subscribe to some new event based on the one. Or it might unsubscribe. Point is, the event processing will probably experience recursive calls, and those calls might edit the data structure that the event processing uses. That could get quite tricky, and it seems possible, even probable, that some arrangements might lead to infinite loops or recursions. We’ll need to be very careful about this.
Let’s sketch out how a simple interaction might go. Suppose there is a cell with some item in it, some identifiable picture to be worked out. Dot steps on that, and in another nearby cell, another thing suddenly appears.
- Key(ContentItem) is in some cell and subscribes to “reveal_key”, providing a call back.
- Switch(ContentItem) is in some other cell.
- Dot steps on Switch. Switch publishes “reveal_key”.
- Key receives a call_back. parameters to be decided as we go.
Another design occurs to me. We could require that the dungeon designer link objects together more explicitly, so that the sequence would be something like this:
- Switch is created and placed.
- Key is created, given Switch as a creation parameter, and placed.
- Key tells switch directly to call Key back when Switch is triggered.
- Dot steps on switch. Switch calls Key. Key appears.
It seems to me that this puts an unnecessary burden on the dungeon designer. With the publish-subscribe, we could add in a new object at any time, and it can be sensitive to all the other things that go on, without a need to know those objects. I think we’ll stick with the publish-subscribe idea.
Because the Dungeon is at the center of things, I think that, at least for now, we’ll have it be the object that is passed around, as it is now, and so when you want to publish something you say dungeon.publish(whatever).
OK, let’s try this idea out. We’ll start with the switch/key concept and TDD up a little publish-subscribe experiment. New test file.
class TestPubSub:
def test_hookup(self):
assert False
Fails. I always do that because I do not trust myself, and PyCharm, to correctly find the new test. It nearly always works, but if it doesn’t, I know that something has gone wrong.
I think we’ll have a PubSub class and sketch out behavior.
class TestPubSub:
def test_exists(self):
pub_sub = PubSub()
This also fails, asking for this:
class PubSub:
def __init__(self):
pass
OK, what can this baby do for us? Let’s do a little usage test.
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
We can fake this up, and just for practice, we’ll do that.
class PubSub:
def __init__(self):
self.callback = lambda event: None
def subscribe(self, event, callback):
self.callback = callback
def publish(self, event):
self.callback(event)
Test runs green. Commit: initial pubsub
Harder test.
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
Fails having not called_sub_1, expected because we overwrite the single subscription with the second subscribe call. We need a list, no, a dictionary I think.
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
Green. Commit: allowing multiple subscriptions.
I am a bit concerned, because PyCharm wrote more of that code than I expected. It’s exactly what I intended to do, which is fine, but how did it know?
It appears that there is a built-in “AI Assistant” which was enabled. I’ve disabled it to see if it makes any difference. I really don’t want to be foxed into using any of the real external LLMs, though an LLM built into my IDE, based on my own code, might be OK.
This is a bit troubling. Anyway we’re here to work on PubSub.
Let’s test two different events. I think they’ll work already.
def test_two_events(self):
called_event_1 = False
called_event_2 = False
def event_1_callback(event):
nonlocal called_event_1
called_event_1 = True
def event_2_callback(event):
nonlocal called_event_2
called_event_2 = True
pub_sub = PubSub()
pub_sub.subscribe('event1', event_1_callback)
pub_sub.subscribe('event2', event_2_callback)
assert not called_event_1
assert not called_event_2
pub_sub.publish('event1')
assert called_event_1
assert not called_event_2
pub_sub.publish('event2')
assert called_event_2
assert called_event_1
With the AI plugin turned off and a restart, PyCharm still did a good job of predicting my code here. I am satisfied that it’s not sending out to Claude or something. But it is still very clever, which is OK by me.
Green. Commit: testing multiple events.
Let’s reflect. We might be at a good stopping point, though I’ve only been at this for about an hour.
Our PubSub could certainly be used to rig up a pair of content items that communicate. We don’t have any parameters in our messages, so there are surely things that we’ll want that aren’t yet possible. But I bet we can make a Key appear when Dot steps on something.
This will be kind of experimental. We’ll add a PubSub to Dungeon, exposing publish and subscribe methods. I see no reason to expose the PubSub itself.
class Dungeon:
def publish(self, event):
self.pub_sub.publish(event)
def subscribe(self, event, callback):
self.pub_sub.subscribe(event, callback)
We could test that, but I don’t think we’ll bother. Now let’s have two little content items surrogates. In the fullness of time we’ll probably have an abstract base class for this but for now we’ll duck type.
No, by golly, I’m going to subclass ContentItem, to pick up its drawing and such. I slam these into place:
class Button(ContentItem):
def __init__(self, name, resource=None):
resource = ':resources:images/items/flagGreen1.png'
super().__init__('button', resource)
def interact_with_player(self, dungeon):
dungeon.announce_received(self.name)
dungeon.publish('button_pressed')
class InvisibleKey(ContentItem):
def __init__(self, name='vanishing key', resource=None):
resource = ':resources:images/items/keyYellow.png'
super().__init__(name, resource)
self.visible = False
def placed(self, dungeon):
def callback(event):
self.visible = True
dungeon.subscribe('button_pressed', callback)
def draw(self, cx, cy, size):
if self.visible:
super().draw(cx, cy, size)
I added a new method, placed, which Dungeon calls after placing an item, giving it a chance to subscribe or whatever things it might need to do:
class Dungeon:
def place_content_at(self, cell, content):
self.contents[cell.xy].append(content)
content.placed(self)
And I add this to main:
dungeon.place_content_at(Cell(29, 30), item_3)
flag = Button('flag')
dungeon.place_content_at(Cell(34, 28), flag)
key = InvisibleKey()
dungeon.place_content_at(Cell(36, 28), key)
And here are the before and after pics, as Dot moves over to step on the flag, note that two spaces further right, a key appears.


As an experiment, this is a success. As a feature, not yet. When she steps on the flag, she gets the message “you have received a flag”. That’s not desired. And I am sure that if she were to step on the key’s cell before the key was visible, she would receive it anyway.
We’ll need to sort that out next time: we have more than enough here to demonstrate to the Product Definition Personality, and while we have no parameters being passed and such, we have a little connection between two objects in the dungeon that do not know each other but nonetheless create a very nice interaction.
Summary
I don’t know where or how the Publish-Subscribe idea originally entered my mind, but it was there to be plucked this morning, and we have a rudimentary but already fairly capable mechanism in place. There is of course plenty to do, including but not limited to:
- Provide for removing subscriptions even while publication is going on;
- Provide for adding new subscriptions as well. Again, deal with what happens when publication is running.
- Very likely we’ll just copy the list of subscribers, allowing the original to be updated harmlessly.
- Sort out the announcement logic, so that messages don’t all default to “You have received …”.
- Produce a reasonable hierarchy of ContentItems, including the ability to change their look as things happen, not just appear and disappear.
There’s lots to do. There always is. But we have a pretty reasonable first implementation of action at a distance, enough to calm the nerves of our PDP.
Cheers! See you next time!