Beauty of Mathematical Curiousity

Once there was a girl who thought math was pretty, and wanted to program her own games. This is her story.

Unfortunatly for her, one of her setbacks was impatience.

In this story, I lay the groundwork for a brighter tomorrow. I have prepared a basic home for my thoughts, and filled it with bright, cheery colors which hopefully don't look too awful. I have neglected spell checking, but everything looks okay. If I've misspelled something please correct me. I don't approve of careless spelling errors any more than lazy capitalization and punctuation.

Now we're adding a window, which will display a very simple clock (a circle and a line). We're going to build a more user-driven program too. We'll allow the user to pause (with any keystroke). Last, we'll add some text to the screen (the time). Somewhere in the meantime, I threw in a small change to Manager. Manager now expects the Event class, not Event.kind. Compare the code I provide in this example to the previous post.

The First Graphic View

I call this one Graphic Clock View. You may skim over this part, because I'm going to replace it in short order. It's more an intermediate step than anything else. This view is going to set up a pygame screen, and store some information about the clock (center point, radius). The 'hand' of the clock will be a simple line from the center to the edge, which measures time in seconds from the program start (that is, it will return to the top of the circle approximately every second).

Calculating the position of the clock hand requires a bit of math. Just a tiny bit of algebra, which you should review if this isn't apparent. We'll use the tick value in seconds times the angle of a full circle (360 degrees, or 2 pi) and calculate the endpoint of the line (x and y) from the angle with sine and cosine. If we don't multiply by the radius, the values will be too small. We have to add the origin, or the end of the line will be in its own world, rather than relative to the center of the circle.

class GraphicClockView(Listener):
    def __init__(self,manager):
        manager.register(TickEvent, self)
        self.size = (400, 400)
        self.origin = (200,200)
        self.radius = 70
        self.screen = pygame.display.set_mode(self.size)
        self.screen.fill( black )
        
        pygame.draw.circle( self.screen, white, self.origin, self.radius, 1)
        pygame.draw.line( self.screen, white, self.origin, (self.origin[0], self.origin[1]+self.radius) )
        pygame.display.flip()

    def notify(self, event):
        x = self.origin[0] + self.radius*math.cos( 2*math.pi*event.tick/1000 )
        y = self.origin[1] + self.radius*math.sin( 2*math.pi*event.tick/1000 )

        self.screen.fill(black)
        pygame.draw.circle( self.screen, white, self.origin, self.radius, 1)
        pygame.draw.line( self.screen, white, self.origin, (x,y) )
        pygame.display.flip()
You may notice that this class does all the work with the window, so we can't easily add more pieces to the display. Like I said, this class would be short lived.

You might think we're ready to run it now (I sure did), but if you do it will crash. Somehow, a quit event gets sent to this views notify method, and the program crashes because the Quit Event does not have a tick attribute. I know this because I ran my program, thinking all was well. Look over the new view carefully - there doesn't seem to be anything wrong. It's registered to listen only to Tick Events. After searching around, I found a fairly ugly bug in Manager.post.

The problem in the Manager is obvious, once you know what it is. Think about it a moment - the program runs fine until Quit Controller give us a Quit Event. There's one significant differnece between these events and all the others - sync. Now that we know exactly where to look, we can see that the last else clause in post is wrong. This else covers when a non-sync event arrives (assuming it is not, for some reason, a tick event). The problem is this:

for m in self.managers:
    self.managers[m].post(event)
This posts the non-sync event to everything, even if it has not registered for that event. Instead, non-sync events should still only be posted to listeners which have registered to hear them, like any other event. Change those two lines to the following:
if event.kind in self.managers:
    self.managers[event.kind].post(event)
Now running the program works exactly as expected - a window pops up, showing a simple 'clock' display. The window closes automatically when the Quit Controller reaches the quit condition and sends the message to exit. The clock "animation" is a little rough looking. Changing FPS to 50 smooths it out quite a bit. That said, I'll leave it at 30.

Some User Interaction

We'll finally provide a bit of user interaction. We'll remove the Quit Controllers, and run the program until the user closes the display window (or finds another way to quit, like ctrl+c). We'll also allow 'pausing' the clock. Note that we do not want to pause the Tick Generator - this will cause the program to hang, waiting for an event which cannot be posted until a Tick Event arrives. We could muck around with non-sync events to unpause, but that's getting a bit messy. And we may find the need to 'pause' the game without hanging up everything (such as games that pause and provide a menu at the same time). We will use a Game Timer Model to handle "game time", while the Ticks will track "real / computer time".

First we'll add the new events. We'll need Game Timer and Pause events. Like most other events, these are very simple. Pause is of the same form as every other event. Game Timer has one complication: in init, it sets a value to gametimer if none is provided. The value it uses is the current tick (pygame.tick.get_ticks()). The way I'll use this event, it doesn't really need this condition, so I'll skip showing the events til the code at the end.

Next we need Game Timer Model. This can be paused, and tracks the total time while not paused. To understand the implementation of this class, I need to explain how I'm going to be using Pause Events. When the user generates a pause event, the value of its pause attribute will be the current tick from pygame.tick.get_ticks(). This way, the model knows exactly when it was paused or unpaused.

The model will need to listen to both Tick and Pause events. This means that the notify method will have to do some "type" checking on events. In order to keep track of the time, we'll need to know the running total ('time'), the last tick value we used ('last'), and the pause status ('paused'). Here is what the init method looks like:

class GameTimerModel(Listener):
    def __init__(self, manager):
        manager.register(TickEvent, self)
        manager.register(PauseEvent, self)

        self.manager = manager
        self.last = 0
        self.time = 0
        self.paused = False

The behavior of the model depends on if it is paused. So naturally, the notify method should have an if paused statement. It's important to think about what we need to do in each condition, and with each type of event, or we'll end up with the wrong behavior for the timer. My first try didn't really pause worth anything, it's value was always the same as the current Tick value, give or take 30 milliseconds. So first, if the model is paused, we should ignore tick events. However, a pause event should unpause the model. Moreover, when we unpause we need to update the value for last, or all the paused time will be added in when we next update the model time. This leads us to:

    def notify(self, event):
        if not self.paused:
            pass
        else:
            if event.kind == PauseEvent.kind:
                self.paused = False
                self.last = event.pause

Next we need to consider what action to take if it is unpaused. Handling tick events is straight forward - update the time with the difference, and set last to the tick value. What do pause events need to do here? We need to update the time still, since time has passed between the last event and now, when it becomes paused. We obviously need to set paused to True. Do we need to update last? It seems like we might, but remember, nothing will be updated when it is paused, until the next pause event (which sets the value for last), so we don't need to pause. Here's the missing chunk of the notify method:

        if not self.paused:
            if event.kind == TickEvent.kind:
                self.time += event.tick - self.last
                self.last = event.tick
            elif event.kind == PauseEvent.kind:
                self.paused = True
                self.time += event.pause - self.last
                #self.last = event.pause
There is one last thing. We still need to send out events. Since I'll be doing some (simple) graphic work, we'll be posting an event every time notify is called. In the future, it would be better to only send out updated values, and let the drawing do more work.

We still haven't got any user interaction. We need a controller. I have foolishly named it KeyboardController, since I planned to use it to register keyboard use for Pause events. However, this controller will also detect and handle other user actions - so you can expect this to be renamed in future examples. While I'm at it, I will probably rename Event Manager and Manager classes to be a bit more descriptive (particularly Manager). This controller will only listen to tick events. It just needs to regularly check for user actions. The actions we will check for is QUIT (when the user closes the program window), and key down actions. Currently, hitting any key will send a pause event. This class is pretty simple:

class KeyboardController(Listener):
    def __init__(self, manager):
        self.manager = manager
        manager.register(TickEvent, self)

    def notify(self, event):
        for event in pygame.event.get():
            if event.type == QUIT:
                self.manager.post(QuitEvent())
            elif event.type == KEYDOWN:
                self.manager.post(PauseEvent( pygame.time.get_ticks() ))
And there we have it! A way to gather information from the user.

Of course, we need to do a little bit in main to make use of this model and controller, but first, lets get some more complicated views for our window.

More Views for the Window

Now we'll replace GraphicClockView. I want to have a clock and some text on the screen. Since these are two separate things (which may or may not have any relationship), we need to have an independant screen. I've chosen to create Screen View for this purpose. Screen view has a few simple requirements. It must initalize the screen, and it must do the redrawing (flip). If we let the clock and text views (which I'll be getting to soon) do the screen flip, it will happen twice as often as needed, or more if we have many graphical displays on the screen. These individual chunks of the screen also must not fill the screen with color, or they'll clobber one another; this is another task for the Screen View.

class ScreenView(Listener):
    def __init__(self, manager):
        pygame.init()
        manager.register(TickEvent, self)
        self.screen = pygame.display.set_mode( (400,500) )
        self.screen.fill(black)

    def notify(self, event):
        pygame.display.flip()
        self.screen.fill(black)
Notice that the screen flips the image (displays all the changes made by other views) and then fills the screen to black on every tick event. If we did not fill the screen each time, the images would be drawn over the old ones, making the clock view fill the circle rather than have a moving hand, and making the text unreadable.

There are a number of requirements for the clock and text views. First, they must be able to react to different types of events, much like Quit Controller. Second, they must be given a rect to define where to draw. For the text, the rect is used only for positioning, but for the clock it also defines the size. In fact, Screen View should really use a rect as well, making it easier to change the window size. Since most of the core of these classes is in drawing a circle (code from the earlier clock) or drawing text to the screen using pygame (which I don't feel the need to cover - I learned it on the fly from the documentation, and it's only a few lines of code), I'll just spew out the code. I named these Display Clock View and Display Text View.

class DisplayClockView(Listener):
    def __init__(self, manager, clazz, screen, rect):
        manager.register(clazz, self)

        self.screen = screen
        #self.rect = rect
        self.origin = (rect[0][0]+rect[1][0]/2, rect[0][1]+rect[1][1]/2 )
        self.radius = rect[1][0]/4 # should do a min

        self.attr = clazz.kind

    def notify(self, event):
        x = self.origin[0] + self.radius*math.cos( 2*math.pi*event[self.attr]/1000 )
        y = self.origin[1] + self.radius*math.sin( 2*math.pi*event[self.attr]/1000 )

        #self.screen.fill(black)
        pygame.draw.circle( self.screen, white, self.origin, self.radius, 1)
        pygame.draw.line( self.screen, white, self.origin, (x,y) )
        #pygame.display.flip()

class DisplayTextView(Listener):
    def __init__(self, manager, clazz, screen, rect):
        manager.register(clazz, self)

        self.screen = screen
        #self.rect = rect
        self.position = rect[0]

        self.font = pygame.font.SysFont('monospace', 20)

        self.attr = clazz.kind

    def notify(self, event):
        surface = self.font.render( str(event[self.attr]), False, white )
        surface = surface.convert()
        self.screen.blit(surface, self.position)
I'll show you next how to use these.

Finally, we'll check out the main class. There's a number of options here. First, I removed the FibonacciModel (since the only thing it does is spam my Event Logger, which I suppose could be turned off since we now have a visual, but I use the Event Logger for minor debugging. I've added the Keyboard Controller, Game Timer Model, and Screen View. These are fairly simple, but the Clock and Text displays are more complicated to set up. Notice the screen view must be constructed first, since we need to pass the screen into the display views. We also set the position and size of the displays, as well as the type of event they listen to. My favorite two combinations are text for gametime, with clock for tick time, or vise versa. It's also kind of interesting to display the Fibonacci Events as text - although everything runs so fast it's just scrolling numbers. Notice that I've provided a standard rect for the text, since we have to blit the text to the screen.

def main():
    manager = Manager()
    manager.add(TickEvent)
    manager.add(FibonacciEvent)
    manager.add(QuitEvent)
    manager.add(GameTimerEvent)
    manager.add(PauseEvent)
    
    #model = FibonacciModel(manager)
    view = EventLoggerView(manager)
    controller = TickGeneratorController(manager)


    screen = ScreenView(manager)
#    clock = DisplayClockView(manager, TickEvent, screen.screen, ( (0,100), (400,400) ) )
#    gametime = DisplayTextView(manager, GameTimerEvent, screen.screen, ( (100,100), (0,0) ) )

    clock2 = DisplayClockView( manager, GameTimerEvent, screen.screen, ( (0,100), (400,400) ) )
    gametime2 = DisplayTextView( manager, TickEvent, screen.screen, ( (100,100), (0,0) ) )
    #fibtext = DisplayTextView( manager, FibonacciEvent, screen.screen, ( (100,50), (0,0) ) )
    
    keys = KeyboardController(manager)
    timeModel = GameTimerModel(manager)

    print manager.managers.keys()
    print manager.managers['quit'].listeners.keys()
    print manager.managers['tick'].listeners.keys()
And that's everything.

full code

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.registerAll(self)

    def notify(self, event):
        """Default implementation for recieving events."""
        pass

class Event:
    """
    A superclass for events to be handled by EventManager
    """
    kind = "generic"
    
    def __init__(self):
        """Set the name of the Event. Subclasses may have other attributes."""
        self.name = "Generic"
        self.sync = True

    def __repr__(self):
        """Custom representation for the event."""
        return self.name + " Event"

    def attributes(self):
        """Return a formated string of the attributes"""
        
        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):
        """Check if the event has a given attribute"""
        return attr in self.__dict__.keys()

    def __getitem__(self, item): 
        """Get the value of an attribute"""
        if item in self.__dict__.keys():
            return self.__dict__[item]
        return None

    #def __getattr__(self, item):
            

class TickEvent(Event):
    """
    Tick events are intended to be time events.
    They dictate the pace of the program.
    """
    kind = "tick"
    
    def __init__(self, time=None):
        Event.__init__(self)
        """Set the name and tick properties."""
        self.name = "Tick"
        self.tick = time
        
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):
        #print "posting  " + event
        #print event.attributes()
        #print "to" + self.listeners.keys()
        
        for listener in self.listeners:
            listener.notify(event)

class Manager:
    """A collection of EventManagers, filtered by event type (kind)."""
    def __init__(self):
        self.managers = {}
        self.events = []

    def register(self, clazz, listener):
        self.add(clazz)
        self.managers[clazz.kind].register(listener)

    def add(self, clazz):
        if not clazz.kind in self.managers:
            self.managers[clazz.kind] = EventManager()
            
    def drop(self, clazz):
        self.managers[clazz.kind] = None

    def registerAll(self, listener):
        """"Register for all managers -
        does nothing if a manager is added afterwards."""
        for kind in self.managers:
            self.managers[kind].register(listener)

    def unregister(self, clazz, listener):
        if clazz.kind in self.managers:
            self.managers[clazz.kind].unregister(listener)

    def unregisterAll(self, listener):
        for kind in self.managers:
            self.managers[kind].unregister(listener)

    def post(self, event):

        if event.kind == TickEvent.kind:
            self.events.append(event)
            temp = self.events
            self.events = []
            #print 'posting some events'
            for e in temp:
                #print e
                if e.kind in self.managers:
                    self.managers[e.kind].post(e)
        else:
            if event.sync:
                self.events.append(event)
            else:
                #for m in self.managers:
                #    self.managers[m].post(event)
                if event.kind in self.managers:
                    self.managers[event.kind].post(event)
        
import pygame, math
from pygame.locals import *
from myevents import *


"""
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 QuitEvent(Event):
    kind = "quit"

    def __init__(self, quitter=True):
        Event.__init__(self)
        self.name = "Quit"
        self.quit = quitter
        self.sync = False

class FibonacciEvent(Event):
    kind = "fibonacci"

    def __init__(self, number=None):
        Event.__init__(self)
        self.name = "Fibonacci"
        self.fibonacci = number

class GameTimerEvent(Event):
    kind = "gametimer"

    def __init__(self, time=None):
        Event.__init__(self)
        self.name = "Game Timer"
        if not time:
            self.gametimer = pygame.time.get_ticks()
        else:
            self.gametimer = time

class PauseEvent(Event):
    kind = "pause"

    def __init__(self, pause=None):
        Event.__init__(self)
        self.name = "Pause"
        self.pause = pause

class TickGeneratorController(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.
    """
    FPS = 50
    def __init__(self, manager):
        self.manager = manager # we will be posting events
        self.manager.register(QuitEvent, self)

        self.running =  True

        self.clock = pygame.time.Clock()

    def run(self):
        while self.running:
            self.manager.post( TickEvent(pygame.time.get_ticks()) )
            self.clock.tick(self.FPS) # throttle back the CPU

    def notify(self, event):
        if event[QuitEvent.kind]: # if it is set to false - keep running
            self.running = False

class QuitController(Listener):
    def __init__(self, manager, goalClass, goalValue):
        self.manager = manager
        self.manager.register(goalClass, self)

        self.goalAttribute = goalClass.kind
        self.goalValue = goalValue

    def notify(self, event):
        #if event.hasAttribute(self.goalAttribute):

        if event[self.goalAttribute] >= self.goalValue:

            event = QuitEvent()
            self.manager.post(event)

class EventLoggerView(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(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):

        manager.register(TickEvent, self)
        self.manager = manager
        
        self.last = 0
        self.current = 1

        manager.post( FibonacciEvent(self.current) )

    def notify(self, event):
        self.last, self.current = self.current, self.last+self.current

        self.manager.post( FibonacciEvent(self.current) )


class GraphicClockView(Listener):
    def __init__(self,manager):
        manager.register(TickEvent, self)
        self.size = (400, 400)
        self.origin = (200,200)
        self.radius = 70
        self.screen = pygame.display.set_mode(self.size)
        self.screen.fill( black )
        
        pygame.draw.circle( self.screen, white, self.origin, self.radius, 1)
        pygame.draw.line( self.screen, white, self.origin, (self.origin[0], self.origin[1]+self.radius) )
        pygame.display.flip()

    def notify(self, event):

        x = self.origin[0] + self.radius*math.cos( 2*math.pi*event.tick/1000 )
        y = self.origin[1] + self.radius*math.sin( 2*math.pi*event.tick/1000 )

        self.screen.fill(black)
        pygame.draw.circle( self.screen, white, self.origin, self.radius, 1)
        pygame.draw.line( self.screen, white, self.origin, (x,y) )
        pygame.display.flip()

# graphic text
        
#class GraphicTickView(Listeners):
#    def __init__(self,manager):
#        manager.register(TickEvent, self)
#        self.


class GameTimerModel(Listener):
    def __init__(self, manager):
        manager.register(TickEvent, self)
        manager.register(PauseEvent, self)

        self.manager = manager
        self.last = 0
        self.time = 0
        self.paused = False

    def notify(self, event):
        if not self.paused:
            if event.kind == TickEvent.kind:
                self.time += event.tick - self.last
                self.last = event.tick
            elif event.kind == PauseEvent.kind:
                self.paused = True
                self.time += event.pause - self.last
                #self.last = event.pause
        else:
            if event.kind == PauseEvent.kind:
                self.paused = False
                self.last = event.pause
                
        # always post game time (on all tick and pause events) 
        self.manager.post( GameTimerEvent( self.time ) )

            
        #if event.kind == TickEvent.kind and not self.paused:
        #    self.time += event.tick-self.last
        #    self.last = event.tick
        #    self.manager.post( GameTimerEvent( self.time ) )
        #elif event.kind == PauseEvent.kind:
        #    if not event.pause:
        #        self.paused = not self.paused
        #    else:
        #        self.paused = event.pause

class KeyboardController(Listener):
    def __init__(self, manager):
        self.manager = manager
        manager.register(TickEvent, self)

    def notify(self, event):
        for event in pygame.event.get():
            if event.type == QUIT:
                self.manager.post(QuitEvent())
            elif event.type == KEYDOWN:
                self.manager.post(PauseEvent( pygame.time.get_ticks() ))

class ScreenView(Listener):
    def __init__(self, manager):
        pygame.init()
        manager.register(TickEvent, self)
        self.screen = pygame.display.set_mode( (400,500) )
        self.screen.fill(black)

    def notify(self, event):
        pygame.display.flip()
        self.screen.fill(black)
                    
class DisplayClockView(Listener):
    def __init__(self, manager, clazz, screen, rect):
        manager.register(clazz, self)

        self.screen = screen
        #self.rect = rect
        self.origin = (rect[0][0]+rect[1][0]/2, rect[0][1]+rect[1][1]/2 )#rect.center
        self.radius = rect[1][0]/4 # should do a min
        #screen.get_width() #self.origin[0]/2 # ideal would be math.min (width, height)

        self.attr = clazz.kind

    def notify(self, event):
        x = self.origin[0] + self.radius*math.cos( 2*math.pi*event[self.attr]/1000 )
        y = self.origin[1] + self.radius*math.sin( 2*math.pi*event[self.attr]/1000 )

        #self.screen.fill(black)
        pygame.draw.circle( self.screen, white, self.origin, self.radius, 1)
        pygame.draw.line( self.screen, white, self.origin, (x,y) )
        #pygame.display.flip()

class DisplayTextView(Listener):
    def __init__(self, manager, clazz, screen, rect):
        manager.register(clazz, self)

        self.screen = screen
        #self.rect = rect
        self.position = rect[0]


        self.font = pygame.font.SysFont('monospace', 20)

        self.attr = clazz.kind

    def notify(self, event):
        surface = self.font.render( str(event[self.attr]), False, white )
        surface = surface.convert()
        self.screen.blit(surface, self.position)






        
black = (0,0,0)
white = (255,255,255)

def main():
    manager = Manager()
    manager.add(TickEvent)
    manager.add(FibonacciEvent)
    manager.add(QuitEvent)
    manager.add(GameTimerEvent)
    manager.add(PauseEvent)
    
    #model = FibonacciModel(manager)
    view = EventLoggerView(manager)
    controller = TickGeneratorController(manager)


    screen = ScreenView(manager)
#    clock = DisplayClockView(manager, TickEvent, screen.screen, ( (0,100), (400,400) ) )
#    gametime = DisplayTextView(manager, GameTimerEvent, screen.screen, ( (100,100), (0,0) ) )

    clock2 = DisplayClockView( manager, GameTimerEvent, screen.screen, ( (0,100), (400,400) ) )
    gametime2 = DisplayTextView( manager, TickEvent, screen.screen, ( (100,100), (0,0) ) )
    #fibtext = DisplayTextView( manager, FibonacciEvent, screen.screen, ( (100,50), (0,0) ) )
    
    keys = KeyboardController(manager)
    timeModel = GameTimerModel(manager)

    print manager.managers.keys()
    print manager.managers['quit'].listeners.keys()
    print manager.managers['tick'].listeners.keys()

    controller.run()

if __name__ == "__main__":
    main()
    
        

Labels: , , , ,

0 comments.
Leave one.
Comments

Post a Comment