Model / View
There’s something not quite right about the design. Can we suss it out, or should we make it a bit worse? Lots of musing, very little code.
General Wisdom
Received wisdom would have us keep “model” and “view” separate. Often the need for this is quite clear. Imagine a bank account, recorded in some database, represented by a raft of transactions, plus a current and pending balance, plus various account information like owners, addresses, tax numbers, and so. That will all be represented, one would hope, in some computer-oriented format. But when we log into the account on its web site, we get displays of that information spread over several pages. When we use the phone app, we get other displays, and when they mail us a statement, there’s yet another display of the same information.
So the view essentially translates the computer’s information into something more suitable for human consumption. And the upshot is that there is plenty of design advice out there telling us to carefully separate the model and the view or views, and, often, to separate the “control” information from those. And it’s good advice, because when we keep these concerns separated, the changes we need to make tend to affect less code, and in simpler ways, than if we let things wind together.
There’s really nothing special about model, view, controller, except that they are commonly valuable ways of thinking about what our program does. But we will find that it is always good to separate concerns. The notions of cohesion and coupling are more general ideas pointing to the same notion: it’s better when one chunk of code is there essentially to deal with just one kind of thing, and it’s better when one chunk’s relationship to another is as limited as possible.
Cohesion: keep similar things together and different things apart. Coupling: keep the relationships between things as simple as possible.
As We Actually Behave
It seems to me that my brother GeePaw Hill tends to program with a more strict adherence to ideas like model/view than I do. I could be wrong, but it seems that way when I watch him program. But there is, I think, a larger difference between the way we work. Not to put too fine a point on it, but I think he tries to keep the program “right” harder than I do. I try to keep the program “working” but I let it get to be pretty far from “right”, and then I notice that it’s not so good, and I improve it.
“Make it work, make it right, make it fast.” – Kent Beck
Truth be told, though I will often refer to Kent’s advice there, and lean on it when I need to, I work the way I do for at least two reasons, one of them possibly good. The possibly good reason is that I want the code I write about to start to go bad, because a fundamental message that I want to get across is that we can always improve the design, while continuing to deliver the features and capabilities that our organization needs or wants. The not so good reason is that I really enjoy refactoring my way out of trouble and so I am prone to get into trouble.
Another possibility is that I’m just not very good at this, but I don’t buy that. I’m actually somewhere in top half of all the programmers I’ve ever met. I’m also well below the level of a few of the ones I’ve met. College vs NBA, or maybe a really good high school vs NBA. Not false modesty: I’ve had the privilege of encountering some amazing people. But I digress.
Our Dungeon Situation
It seems to me that there is a pretty clear conceptual line between the Dungeon and our view or views of it. The Dungeon thinks in terms of cells at integer coordinates, with items in some of those cells, and with an Intrepid Adventurer, Dot, who moves from cell to cell, interacting with whatever she encounters. And we can say with confidence that there will be programmed objects that also wander the dungeon, and that they’ll interact with Dot in various interesting ways.
And we have a view of that Dungeon. It currently displays each cell as a square, nominally 16x16 pixels, with a little image representing Dot or the content items. We can foresee with confidence that there will be additional displayed information, such as Dot’s health, and her inventory of discoveries. We may also provide a “mini-map”, a small picture showing a map of the dungeon Dot has so far explored, representing the map she would draw if she were wise.
So in principle, we have a view of the cells, a view of content items, a view of Dot, and someday, views of dungeon denizens, and more. And we have a Scroller that scrolls messages on the screen. It is another view.
So far so good, but the current situation seems odd to me, and a little bit off. Let me explain.
We have a DungeonObject that holds onto everything. I think that’s just about right. We have a DungeonView object, which holds onto the Dungeon, and it actually runs Arcade, bringing up the game window and so on. DungeonView acts as controller as well as view, reading keyboard events and translating them into suitable calls to Dungeon. And I am OK with that.
The DungeonView has a somewhat extensive setup: there’s quite a bit to do to get ready to run:
class DungeonView:
def setup(self):
self.setup_scroller()
self.setup_cameras()
self.shape_list = arcade.SpriteList()
self.create_rooms(self.shape_list)
self.content_list = arcade.SpriteList()
self.create_content_list(self.content_list)
The DungeonView maintains two sprite lists, one that contains the Dungeon’s flooring, which is randomly created and then stays the same, and one that contains the content items.
The flooring is created by this code:
class DungeonView:
def create_rooms(self, shape_list):
for room in self.dungeon.rooms:
RoomView(self.dungeon, room).create(shape_list)
For each room in the dungeon, we create a RoomView, and call its create method, which adds flooring into the shape_list. We could rename that method. Let’s look at it and see what it does.
class RoomView:
def create(self, shape_list):
cells = set(self.room.cells)
for cell in cells:
self.choose_flooring(cell, shape_list)
Yes. Let’s rename that to create_floor. That’ll be more clear. The RoomView does nothing else, really. It creates the floor, choosing the floor and boundary tiles, filling in the flooring sprite list. And I think that’s OK. There’s no reason to retain the RoomViews, as they have no other behavior after deciding the flooring.
But then DungeonView creates the second sprite list, the contents list:
class DungeonView:
def create_content_list(self, content_list):
for content in self.dungeon.contents.values():
for item in content:
sprite = item.get_sprite()
content_list.append(sprite)
What are those items? We can see how they are created, which may someday be done in a more clever way, to create levels of the dungeon, but right now they are created by hand in main:
main.py
...
resources=':resources:images/items/'
my_resources = '/Users/ron/Desktop/DungeonTiles/png/objects/'
item = ReceivableContent("a mysterious key", resources + 'keyRed.png')
dungeon.place_content_at(Cell(29, 28), item)
item = ReceivableContent("a precious sapphire", resources + 'gemBlue.png')
dungeon.place_content_at(Cell(29, 29), item)
item = ReceivableContent("another sapphire", resources + 'garbled.png')
dungeon.place_content_at(Cell(29, 30), item)
item = DrawableContent("skel", my_resources + 'Skeleton1.png', scale=0.5)
dungeon.place_content_at(Cell(30, 32), item)
item = DrawableContent("skel", my_resources + 'Skeleton2.png', scale=0.5)
dungeon.place_content_at(Cell(30, 26), item)
item = DrawableContent("chest", my_resources + 'chest/1.png', scale=0.5)
dungeon.place_content_at(Cell(34, 25), item)
flag = Button('button')
dungeon.place_content_at(Cell(34, 28), flag)
key = SecretKey()
dungeon.place_content_at(Cell(36, 28), key)
There are a few “content” classes at present, and they all adhere to this abstract definition:
class Content(ABC):
@abstractmethod
def draw(self, cx, cy):
pass
@abstractmethod
def interact_with_player(self, dungeon):
pass
@abstractmethod
def placed(self, dungeon):
pass
This is where the model/view line seems blurred to me. Here’s one of the concrete classes, the currently most complex, the SecretKey, which is invisible until the Button is pressed and then becomes visible and can be given to Dot if she steps on it:
class SecretKey:
def __init__(self, name='a secret key', resource=None):
resource = '/Users/ron/Desktop/DungeonTiles/png/objects/Key (1).png'
self.name = name
self.painter = ContentPainter(resource, scale=0.5)
self.set_visibility(False)
def draw(self, cx, cy):
self.painter.draw(cx, cy)
def get_sprite(self):
return self.painter.sprite
def interact_with_player(self, dungeon):
if self.visible:
dungeon.receive_item(self)
dungeon.announce(f'You have received {self.name}!')
self.get_sprite().remove_from_sprite_lists()
def placed(self, dungeon):
self.set_visibility(False)
def callback(event):
dungeon.announce('A key has appeared!')
self.set_visibility(True)
dungeon.subscribe_once('button_pressed', callback)
def set_visibility(self, aBoolean):
self.visible = aBoolean
self.painter.set_visibility(aBoolean)
This is the only content item that actually uses placed. The fact that that the others all pass suggests that there’s something off about placed or about the SecretKey.
I happen to know that draw is called a zillion times per second, and because the content items are all in a sprite list, there is no real need to call the draw function on them. Currently the draw sets the item’s position on every call. That apparently doesn’t really bother the sprite list, but it’s not really right.
If placed were to be called with the cell’s drawing coordinates, most of these items wouldn’t need to have draw do anything but pass. I do think we have to give them a chance to draw, in case they change form or something.
interact_with_player is called by the Dungeon, with a pointer to the dungeon. That’s not quite OK: this object is a view of content, and the dungeon shouldn’t really have a pointer to a view. But our ContentItem is part model, understanding interact_with_player, and, arguably, even receiving a callback from the dungeon is a model thing, not a view thing.
So it gets fuzzy.
Do Something
I got carried away with explaining, which is part of how I think about things, explaining them here, since my rubber duck has been misplaced. We need to do something. “Something” includes “something else entirely”, but I’ve brought us this far, I’d like to make some progress on improving things at least a bit.
One quite reasonable idea might be to have there be a separate content item, and content item view, which would hold the content item. But right now, we have what it is, and how it is to be drawn, bound together in main:
item = ReceivableContent("a precious sapphire", resources + 'gemBlue.png')
dungeon.place_content_at(Cell(29, 29), item)
Hm. Thinking as a player of the game, content has those two aspects, what it does in the game, and what it does on the screen. We have a bit of pluggable behavior, Method Object, Strategy-like code now, in the ContentPainter object:
class ContentPainter:
error_texture = ':resources:images/items/star.png'
def __init__(self, resource, scale=0.5):
self.scale = scale
try:
texture = arcade.load_texture(resource)
except (FileNotFoundError, AttributeError):
texture = arcade.load_texture(self.error_texture)
self.sprite = Sprite(texture)
self.sprite.scale = scale_texture(self.sprite.texture, self.scale)
def draw(self, cx, cy):
sprite = self.sprite
sprite.position = (cx, cy)
def set_visibility(self, aBoolean):
self.sprite.visible = aBoolean
self.sprite.alpha = 255 if aBoolean else 0
You are waffling, sir!
I want to find a tiny change, almost any tiny change. I want to kick the code out of balance, in any direction, with the expectation that making it different will lead to understanding. Certainly this two hour meeting hasn’t gotten us anywhere.
I want to get rid of the draw method that doesn’t really draw anything. Slightly further out, I think I want to get rid of those content classes or turn them into different views, not different kinds of content. And then we could let Content be whatever it wants to be, and views be whatever they want to be.
To get rid of draw, we’ll need to give the item its coordinates, to give to the sprite, once. Do that where we create the sprites:
class DungeonView:
def create_content_list(self, content_list):
for content in self.dungeon.contents.values():
for item in content:
sprite = item.get_sprite()
content_list.append(sprite)
We need the items from the dungeon contents, which include the cells, if I recall.
def create_content_list(self, content_list):
for cell, content in self.dungeon.contents.items():
position = cell.center_position(cell_size)
for item in content:
sprite = item.get_sprite(position)
content_list.append(sprite)
That requires a change to get_sprite
def get_sprite(self, position):
self.painter.set_position(position)
return self.painter.sprite
That has to be done is a few places, a clue that the objects aren’t right yet. And in ContentPainter:
def set_position(self, position):
self.sprite.position = position
I also set the draw method to pass. I expected everything to work, and was almost right. The display is good but when you step on an item, it no longer disappears. Ah, the program sort of crashed. We are using get_sprite. Let’s not do the setting there. Reset, do again.
def create_content_list(self, content_list):
for cell, content in self.dungeon.contents.items():
for item in content:
sprite = item.get_sprite()
sprite.position = cell.center_position(cell_size)
content_list.append(sprite)
We’re a view. The spite belongs to us. We can set its position. Simpler. We can stop calling draw in draw_content_list, just drawing the sprites:
def draw_content_objects(self):
# for cell, content in self.dungeon.contents.items():
# cx, cy = cell.center_position(cell_size)
# for item in content:
# item.draw(cx, cy)
self.content_list.draw()
Now the content items can have pass for their draw. I’m reluctant to remove it. No, that’s fear talking. Remove all the draw methods from the content items and the Content abstract class.
Everything works. Commit: removed draw from content items and content painter.
Something rather nice has happened.
The content subclasses have a rather nice separation between dealing with the dungeon and dealing with the sprite, with one exception. Here’s ReceivableContent:
class ReceivableContent(Content):
def __init__(self, name, resource=None):
self.name = name
self.painter = ContentPainter(resource, scale=0.5)
def get_sprite(self):
return self.painter.sprite
def interact_with_player(self, dungeon):
dungeon.receive_item(self)
dungeon.announce(f'You have received {self.name}!')
self.get_sprite().remove_from_sprite_lists()
def placed(self, dungeon):
pass
They all create a sprite in their init and they can all return it with get_sprite. (Duplication arrgh.)
Otherwise they talk only to the dungeon, which is passed to them when they need it. Almost.
The exception, for now, is the SecretKey, which listens for the Button and sets its visibility on and off. Setting visibility is a view function, and refers to the sprite. And there will be a few more things like that. The Button itself should have two textures, one for unpressed and one for pressed, and that too will be a view issue.
So it’s not clean yet … but it is a bit more clean than it was.
The ContentPainter is clearly not a painter. It is, at best, a nearly trivial wrapper around a sprite. Its main function is to set up and scale a sprite, and to be able to set its visibility.
Ah, and that is interesting now that I look at it. There are two similar things that happen on the screen: a thing is either there or not. The one case, it wasn’t there and now it is. In the other, it was there and now it isn’t. We have two mechanisms, one that removes the item from the sprite list, and one that tells it to be transparent. We might, instead, have an InvisibleContent, containing the key, and have the Key be a standard receivable object that can remove itself or be removed as needed. When the InvisibleContent, which never draws anything, hears the Button, it places the key and removes itself. The key, as a standard drawable object, would appear.
Except: we currently do not add anything to the sprite list dynamically. But we certainly could: we remove things after all.
So maybe there is a bit of convergence to be found there.
But I’m still left with the question of how the view knows something has happened in the model. Should dungeon view be subscribing to events that the dungeon sends? Perhaps.
But for now … I’ve done enough.
Reflection
I think I wound up adding one line of code and removing about 15 or 20. But those lines were blurring the boundaries between two concepts, and they were also banging away setting and resetting the same value over and over, which was probably irritating to the sprite lists, long-suffering and patient though they surely are.
I’m leaning toward a simple wrapper for the content objects, basically little more than a sprite and supporting methods to deal with visibility and perhaps position if we need that, and we probably will. The content, we might try to pare down to a single class, probably including some conditional statements to deal with levels of interaction, and with an eye to replacing the conditionals with Method or Strategy objects, or perhaps just pluggable methods.
And it is possible, even probable, that we should create a couple of different content objects, because both the Button and the SecretKey are awfully specific and should be made up of some kind of kit of behavior rather than each one custom made.
Summary
Sometimes it goes like this. Sometimes I need to think, so I write pages of text explaining my thoughts. If I had a team or a pair with me, the conversation would take much less time than the writing and reading, but I have to deal with the situation as I find it, and I’m here alone except for my wonderful wife, who, wonderful though she is, is no help in a programming situation.
So I think and write and write and think and then write a little code, or a lot, depending on the day. Never a whole lot, since the point is to do things in small steps.
Anyway, I’m for some chai and a granola bar, which is arguably just as nutritious as tea, and a bowl of cereal with milk.
See you next time!