Now we'll add back in Fibonacci events. After all, wouldn't it be great if we could also quit based on reaching / passing a certain point in the Fibonacci sequence, such as numbers higher than 1000? As such, we'll upgrade the Quit Controller. All this will require a minor change to Events, and a major overhaul for EventManager. To preserve my earlier examples, I copied myevents.py to myevents2.py, so if you notice this file imported and referenced in the new code, that's why.
I was running amok for a while with this bit, and having some trouble with debugging. Due to my impatience, I didn't keep close track of what changes had what effects. In short, however, trying to change the Quit Controller to stop when the Fibonacci sequence was past a certain value just caused it not to quit at all. I'm not going to put time into trying to recreate the bug, although I'm fairly certain I could. Instead, I'll just make notes of the changes and why they were needed.
Posting Fibonacci Events
Very simply, I'll be putting the Fibonacci model back to its first iteration - posting number events. Of particular importance is a change to the notify method:
def notify(self, event): if event.hasAttribute('tick'): #without the if - just runs fib events forever triggered by fib events (one tick) self.last, self.current = self.current, self.last+self.current event = Event() event.number = self.current self.manager.post(event)The major differnce here is the if clause - before it also triggered on it's own events (no wonder recursion was a problem, eh?).
However, there's more to do that just fix recursion problems. Next comes the Quit Controller. I'm no longer using the same class, but I've left it in my code to illustrate some of the trouble I was having. I've since tracked the problem down to a poorly implemented EventManager, but at the time I was flailing a bit. You can see the QuitAtGoalController at the end, when I post the full code. In the meantime, I decided I should replace this class with a controller that allowed multiple conditions. In addition to being more general (good coding practice and all), I could then have two instances of it, restricting both the maximum Fibonacci value and the number of frames run. If I wanted more fine control of the number of frames, I'd need a simple counter model, a counter event, and to restrict that number instead of the time run. I'm not that particular, so I'll skip that. I could be a nice exercise, particularly if you want to know the Fibonacci number at a particular point in the sequence.
First, I'll explain the change to Event. It will be useful for the code in the new controller. I've added in this method:
def __getitem__(self, item): if item in self.__dict__.keys(): return self.__dict__[item] return NoneThis method allows me to refer to Event attributes using event[attribute] instead of the more painful event.__dict__[attribute]. Here is the new controller:
class QuitController(myevents2.Listener): def __init__(self,manager,goalAttribute, goalValue): self.manager = manager self.manager.register(self) self.goalAttribute = goalAttribute self.goalValue = goalValue def notify(self, event): if event.hasAttribute(self.goalAttribute): if event[self.goalAttribute] >= self.goalValue: event = Event() event.quit = True self.manager.post(event)To replace the old controller, we'll use stop1 = QuitController(manager, 'tick', 30*5). The new Quit Controller will also restrict the size of the Fibonacci numbers, quitting after we pass 1000: stop2 = QuitController(manager, 'number', 1000).
The short story on this class is that we store the event attribute name we want to check, and the value we don't want it to pass (I wouldn't recommoned trying to use an attribute with something like string values for this - but feel free to try it). When it is notified of an event, it checks if the event has the attribute of interest, and if the attribute value is greater than the value provided.
That pretty well covers the changes to the main program - but it still breaks. Tends to run forever, and generally be weird. That's because there's a problem with EventManager.
Unbreaking Event Manager
The basic concept for making the Event Manager work is that it will only post events after a tick event is posted. That is, it stores the list of events it receives, and sends them all out at once when there is a tick. This prevents listeners from sending events and triggering themselfs only to get stuck in infinite recursion. It also forces everything to move at the same pace - which I think will help once I start making a game from this. Finally, it posts events in the order they were created, avoiding a problem mentioned in the tutorial (that the manager gets events A then B, but notifying listeners of event A generates event C, which is then sent out before event B - that is event order in a simpler setup is A,C,B not A,B,C. If my one sentence summary doesn't make sense, go read that note in the tutorial).
I had debugging trouble here too. Perhaps here most of all, actually. I tried to use only one array, but I didn't think anything through before trying to implement it. Don't do that. It's bad. Draw diagrams, pictures, and think about the flow when you go from a simple method to a complex one.
First, I thought I needed two arrays. Then I decided I didn't, but by not thinking things through, I caused bugs and had to debug more. Then I realized how to make it work with two arrays (when I finally figured out what was breaking), and finally, just as I was typing this and thinking about how to explain it, I realized why one array was fine. So I just went and changed the code, ran it again to check it, and here we go.
First, we create an array to store events in. This is so we can process them (notify listeners) when we wish to, rather than as they are received.
class EventManager: """ A manager for coordinating communication between Model, View and Controller """ def __init__(self): """Set up a dictionary to hold references to listeners""" self.listeners = WeakKeyDictionary() self.events = []The register and unregister methods are fine. But the post method needs some serious work. If an event is not a tick event, it is added to the list. Otherwise, we dump all events into a temporary variable, and notify listeners about them. The event list must be cleared before we begin notifying listeners, or it will become a mess of an array containing current events, and the resulting events of current events.
def post(self, event): """ Inform all listeners about an event. CANNOT GUARENTEE THE ORDER THE LISTENERS RECIEVE THE EVENTS """ if event.hasAttribute('tick'): temp = self.events self.events = [] print "posting" for listener in self.listeners: listener.notify(event) for e in temp: listener.notify(e) else: self.events.append(event)Note that when notifying listeners about event, we don't just loop over the event list - we also notify them about the tick event. This is very important! Otherwise, only one tick is run, and the program just hangs and waits. It's bad (this problem may have been combined with other mistakes I made getting to this point. It's all corrected now, so I haven't double checked). Another way to do this would be to add the tick event to the temp array, and then just loop over that. Notice I notified the tick event first, then everything else. There was no reason for that. I could have done the array then the tick event. Since the tick event is a regular clock event, whos value is not significant, it shouldn't matter.
Conclusion
There were some serious struggles with this portion. Mostly I just need to slow down sometimes. That being said, I'd like to point out the output of this stuff. Assuming you restrict time to 1000 (some large number) and the Fibonacci number to 1000, it will quit on the second condition since it will reach that first. This can be confusing, because the value it prints out last is then 2584. That seems quite off from stopping at 1000, but remember a few things. First: this is the Fibonacci sequence - it grows rapidly. Second: the TickGeneratorController run function will run util it gets a quit event. Due to the way everything is set up, the quit event gets to the controller after the model updates the number. So if you look more closely, the first Fibonacci number above 1000 is 1597. This number generates the quit event from the Quit Controller. The number is updated to the next value, 2584, before the program is exited.
A little more clear on why the number gets updated after a quit event is sent. The normal flow (no quits) goes something like this:
Tick -> ... some events recieve and 'post' events (the 'posted' events are stored in the manager, they are not really posted yet)
Tick -> 'posted' events are posted, some events reieve and 'post' events
... and so on, until
Tick -> ... Quit Generated (it is sent to post, but nothing is notified about it until the next tick event) - this is where that last update occurs.
Tick -> ... Quit is posted (running set to False)
Code
Main Code:
import myevents2, pygame from myevents2 import Event # too lazy to fix this everywhere right now... """ Some of these will send out more events than are needed - and they are all of type Event. There is probably some use in having separate listeners cateorgies. I'll try to explore that later. It will also log a bunch of "Generic Events" since I haven't put forth any effort to use event types in a better way. """ class TickGeneratorController(myevents2.Listener): """ Uses a clock to generate a 30 FPS tick. Currently 30 is a magic number. Note that this class is a Listener, but there was really no need to extend the superclass. It is done here for consistency only. """ def __init__(self, manager): self.manager = manager # we will be posting events self.manager.register(self) self.running = True self.clock = pygame.time.Clock() def run(self): while self.running: event = Event() # this should be a tick event, but I'm running super simple for now event.tick = pygame.time.get_ticks() self.manager.post(event) self.clock.tick(30) # throttle back the CPU def notify(self, event): if event.hasAttribute('quit'): self.running = False class QuitController(myevents2.Listener): def __init__(self,manager,goalAttribute, goalValue): self.manager = manager self.manager.register(self) self.goalAttribute = goalAttribute self.goalValue = goalValue def notify(self, event): if event.hasAttribute(self.goalAttribute): if event[self.goalAttribute] >= self.goalValue: event = Event() event.quit = True self.manager.post(event) class QuitAtGoalController(myevents2.Listener): def __init__(self,manager): self.manager = manager self.manager.register(self) #self.goalValue = 100 FPS = 30 # just so I don't forget what the magic number 30 is self.stopAfterTick = 5*FPS #self.stopAfterValue = 100 def notify(self, event): if event.hasAttribute('tick') and event.tick >= self.stopAfterTick: event = Event() event.quit = False # will STILL quit - should fix that self.manager.post(event) #if event.hasAttribute('number') and event.number >= 5: # event = Event() # event.quit = False # self.manager.post(event) class EventLoggerView(myevents2.Listener): """ Logs events to the screen. Note that it does not define init - it uses the default Listener. """ def notify(self, event): print event print event.attributes() class FibonacciModel(myevents2.Listener): """ Stores some simple information and changes itself on updates. This class will generate events so that we can log them. """ def __init__(self, manager): myevents2.Listener.__init__(self, manager) # a long way of saying: register me self.manager = manager # to post events. If I used a different logging method, this would not be needed. self.last = 0 self.current = 1 event = Event() event.number = 1 manager.post(event) def notify(self, event): if event.hasAttribute('tick'): #without the if - just runs fib events forever triggered by fib events (one tick) self.last, self.current = self.current, self.last+self.current event = Event() event.number = self.current self.manager.post(event) def main(): manager = myevents2.EventManager() model = FibonacciModel(manager) view = EventLoggerView(manager) controller = TickGeneratorController(manager) stop1 = QuitController(manager, 'tick', 30*5) # 30 is FPS stop2 = QuitController(manager, 'number', 1000) controller.run() if __name__ == "__main__": main()
Event Code (myevents2.py):
from weakref import WeakKeyDictionary class Listener: """ A superclass for listeners. Note that a listener must save the reference to the manager if it needs to send events, as opposed to just recieve them. """ def __init__(self, manager): """Register with the manager automatically.""" manager.register(self) def notify(self, event): """Default implementation for recieving events.""" pass class Event: """ A superclass for events to be handled by EventManager """ def __init__(self): """Set the name of the Event. Subclasses may have other attributes.""" self.name = "Generic" def __repr__(self): """Custom representation for the event.""" return self.name + " Event" def attributes(self): result = '' for attr in self.__dict__.keys(): if attr[:2] == '__': result = result + "\tattr %s=\n" % attr else: result = result + "\tattr %s=%s\n" % (attr, self.__dict__[attr]) return result def hasAttribute(self, attr): return attr in self.__dict__.keys() def __getitem__(self, item): if item in self.__dict__.keys(): return self.__dict__[item] return None class EventManager: """ A manager for coordinating communication between Model, View and Controller """ def __init__(self): """Set up a dictionary to hold references to listeners""" self.listeners = WeakKeyDictionary() #self.current = [] #self.future = [] self.events = [] def register(self, listener): """Register a listener. It must implement the function notify(event)""" self.listeners[ listener ] = 1 def unregister(self, listener): """Unregister a listener""" if listener in self.listeners: del self.listeners[ listener ] def post(self, event): """ Inform all listeners about an event. CANNOT GUARENTEE THE ORDER THE LISTENERS RECIEVE THE EVENTS """ if event.hasAttribute('tick'): temp = self.events #self.current = self.future #self.future = [] self.events = [] for listener in self.listeners: listener.notify(event) #for e in self.current: for e in temp: listener.notify(e) else: #self.future.append(event) self.events.append(event) """Test the contents of this file.""" if __name__ == "__main__": e = Event() print e em = EventManager() class Test(Listener): """Uses the default init.""" def notify(self, event): print "I have recieved a "+ str(event) test = Test(em) em.post(e) em.unregister(test) em.post(e)
Labels: MVC, pygame, python, series 1, tutorial
Leave one.