Not long ago (this morning, actually), I managed to dig myself into an endless recursive hole. To prevent the need to add links to previous related posts on this topic, I've started a list in the side box. If I run out of topics, I may go back through and put the list in all the posts, but for now it's in the sidebar of the blog.
My short term solution was to remove any events posted by the Fibonacci model. Instead, it will print out values itself. Not as wonderous, perhaps, but I did say it was a short term solution.
The New (but not improved) Fibonacci Model
Basically, I removed all attempts for the model to post events, and replaced them with print statements. It's not glorious, but that's ok.
class FibonacciModel(myevents.Listener): def __init__(self, manager): myevents.Listener.__init__(self, manager) # no longer stores a reference to manager self.last = 0 self.current = 1 # replaces manager.post(event) print "NUMBER: %s" % self.current def notify(self, event): self.last, self.current = self.current, self.last+self.current # replaces a post(event) print "NUMBER: %s" % self.current
Other Problems
Sadly, this isn't nearly enough. Instead it brings to light some other problems with the code.
Checking Event Type
The notify methods (often) work on only one type of event. Here is the code I used before:
if event.quit: # do stuffThis works great... for quit events. But other events, like tick events and the (currently removed) fibonacci events don't have a 'quit' attribute, and the program promptly crashes. I've replaced that with the following.
if event.hasAttribute('quit'): self.running = FalseThis requires a hasAttribute function in the Event class. This is what I was hinting at in my last post when I said something was still missing from Event. We didn't catch the error because we were busy being caught in an infinite loop.
def hasAttribute(self, attr): return attr in self.__dict__.keys()
Scope Error
The Tick Generator also had a scope error, which was not caught before the previous program crashed from recursion depth. Make sure to change clock.tick(30) to self.clock.tick(30).
No Exit
Although we check for quit events, nothing generates them. Unlike most pygame programs, there is no window to close, and no user interaction at all. Here is a great opportunity to use the MVC design to our advantage. Adding something to generate events is easy - I'll add another controller for this. It will still not have any user input, but it will prevent the need to control+C the program to stop it.
I will call this new addition the QuitAtGoalController. It's not a very good name - the goal can only be a tick value and the only way to set it is to edit the code. We'll overlook these shortcomings.
class QuitAtGoalController(myevents.Listener): def __init__(self,manager): self.manager = manager self.manager.register(self) FPS = 30 # just so I don't forget what the magic number 30 is self.stopAfterTick = 5*FPS 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)This is some mediocre code right here. Notice the event I created - it has the quit attribute set, but to False. However, our quit condition is simply that this property exists, not its value. Then there's some good ole' magic numbers. I've identified FPS instead of just putting the value 30 in (or worse - 150, because that could mean anything at all), but this value should really be identified by a variable throughout the program, or by a global constant. Ah well, it's an example after all, and it gets the job done. Anyway, this class is set up to stop the program after 5 ticks, or approximately 150 milliseconds. So we're only looking about 5 steps into the Fibonacci sequence. We just need to create an instance of this class in the main function.
Full Example
The main code:
import myevents, pygame from myevents 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(myevents.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.quit: # I need to define an event with this! if event.hasAttribute('quit'): self.running = False class QuitAtGoalController(myevents.Listener): def __init__(self,manager): self.manager = manager self.manager.register(self) FPS = 30 # just so I don't forget what the magic number 30 is self.stopAfterTick = 5*FPS 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) class EventLoggerView(myevents.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(myevents.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): myevents.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 print "NUMBER: %s" % self.current #event = Event() #event.number = 1 #manager.post(event) def notify(self, event): self.last, self.current = self.current, self.last+self.current print "NUMBER: %s" % self.current #event = Event() #event.number = self.current #self.manager.post(event) def main(): manager = myevents.EventManager() model = FibonacciModel(manager) view = EventLoggerView(manager) controller = TickGeneratorController(manager) stop = QuitAtGoalController(manager) controller.run() if __name__ == "__main__": main()
The Event code, since it's changed a bit:
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() 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() 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.""" for listener in self.listeners: listener.notify(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.