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.

Time to introduce something to make it "game like". This program will still be a far cry from a game, but it's an important step nonetheless. To understand the logic, you need to understand what I'm trying to achieve. The goal is to create a "Grow" game, modeled off the Grow Series, which I find to be a huge source of both pleasure and entertainment. The way I view the logic behind these games is as a tree. Each time a button is selected, that button is disabled and you traverse down a branch of the tree. It's a bit of a combinatorics problem. The more buttons (options) there are, the more complicated the tree. The number of cases to deal with grows very very quickly, so I've chosen to pursue a 3-option game.

combinatorics and permutations

A side trip into math! Here I'll explain why I chose only 3 buttons. I will refer to these buttons as A, B, and C or as 0, 1 and 2. The short version is because of combinatorics. In the game logic, the order the buttons are selected is important. Not only that, but every step will have some sort of result or event. That means that every possible combination of buttons (excluding repeats) will have an event. There is (potentially) an event with 0 buttons selected. Once another button is selected (say, B), there is an event. The next choice (say, A) results in another unique event. That is, this event is distinct from choosing A first, or choosing C first, followed by A. With three buttons, there are events for selections of 0, 1, 2 or 3 button sequences. Mathematically, finding the number of possible combinations for a certain number of choices is a permutation problem.

Given N buttons, a sequence of choices of R buttons (such as a choice of 3 out of the 6 buttons), is n ! / (n - r) ! . So, for N = 6, and R = 3, this is 120. However, we need to know the number of choices for every length of sequence (such as length 0, 1, 2, 3, 4, 5 and 6 out of 6 buttons). So we sum it all up, for R=0..N: n!/(n-r)!

Going through the calculations for 3 buttons gives us 1+3+6+6 = 16 events to deal with. If we used 4 buttons instead, we get 1+4+12+24+24 = 65 events! You can see that the rate of growth of events is pretty ridiculous. For an actual game, it's probably worth having at least 4 buttons (probably more like 6 or 8), but for an example, 3 is more than enough for proof of concept.

Interacting with the user

Once upon a time, I set wrote Mouse Interface. I wrote that planning to write a Quit Interface (and keyboard, etc) to react to the user. It seemed like a good idea, but when I tried to implement it, the Quit Interface never recieved any events at all. Mouse Interface consumed all the pygame events. So instead I replaced it will User Interface, which deals with pygame events.

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

    def notify(self, event):

        event = None
        for event in pygame.event.get():
            if event.type == MOUSEBUTTONUP or event.type == MOUSEBUTTONDOWN or event.type == MOUSEMOTION:
                self.manager.post( MouseEvent(event) )
            elif event.type == QUIT:
                self.manager.post( QuitEvent() )

Game Logic

As mentioned earlier, we need to have an event for every combination of buttons. I am using a dictionary, keyed by integer tuples. We also need to keep track of the current state, which is stored as a tuple to key the dictionary. The game logic has a single method, make_choice. Don't even ask why I randomly used underscores instead of my normal cases. I don't know. I'll fix it later. When we make a choice, we replace the state variable with a new tuple, with the value of the choice "appended" at the end. To keep it super simple, we will just print out the value of the logic tree.

class GameLogic:
    def __init__(self):
        self.state = ()

        self.logictree = {
            () : "Root", (0,) : "0", (1,) : "1", (2,) : "2",
            (0,1) : "01", (0,2) : "02", (1,0) : "10",
            (1,2) : "12", (2,0) : "20", (2,1) : "21",
            (0,1,2): "012", (0,2,1): "021",
            (1,0,2): "102", (1,2,0): "120",
            (2,0,1): "201", (2,1,0): "210"
            }

        print self.state
        print self.logictree [ tuple(self.state) ]
        

    def make_choice(self, value):
        temp = list(self.state)
        temp.append(value)
        self.state = tuple( temp )

        if self.logictree.has_key(self.state):
            print self.logictree[ self.state ] 
        else:
            print self.state
            print "INVALID LOGIC STATE"
The state could also be a list. In that case, it would need to be converted to a tuple to be used as a key. In the full code, you can see how this works in the commented out code.

The meat of the main functions is the action for the buttons. I define another method, chooseButton, to handle the button presses. The action disables the button and sends the choice to the game logic. It's important to disable the button because the permutation only works if each button can only be chosen once. The game is over when all the buttons have been hit.

def main():

    game = GameLogic()
    manager = Mediator()
    user = UserInterface(manager)
    screen = ScreenView(manager, (400,400), black)
    buttons = ButtonMediator(manager)

    def chooseButton( value ):
        buttons.disableButton( value )
        game.make_choice( value )

    buttons.addButton( pygame.Rect(50,50,10,10), screen.screen, chooseButton, 0 )
    buttons.addButton( pygame.Rect(100,50,10,10), screen.screen, chooseButton, 1 )
    buttons.addButton( pygame.Rect(150,50,10,10), screen.screen, chooseButton, 2 )

    controller = TickGeneratorController(manager)

    controller.run()

code

from commoncomponents import *
from myevents import *
from pygame.locals import *
from mycolors import *
import pygame


class ButtonModel:

    # work around for now - I like the notation STATUS.normal better
    NORMAL = 'normal'
    DOWN = 'down'
    HOVER = 'hover'
    DISABLED = 'disabled'
    STATUS = { 'normal':0 , 'down':1 , 'hover':2 , 'disabled':3 }

    def __init__(self, rect):
        self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal
        self.rect = rect

    def enable(self):
        if self.status == ButtonModel.STATUS[ButtonModel.DISABLED]: #.disabled:
            self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal

    def disable(self):
        self.status = ButtonModel.STATUS[ButtonModel.DISABLED] #.disabled
        
    def mouseAction(self, mouseevent):
        if self.status == ButtonModel.STATUS[ButtonModel.DISABLED]: return #.disabled: return

        if self.rect.collidepoint( mouseevent.pos ) :
            if mouseevent.type == MOUSEBUTTONDOWN:
                self.status = ButtonModel.STATUS[ButtonModel.DOWN] #.down
            elif mouseevent.type == MOUSEBUTTONUP and self.status == ButtonModel.STATUS[ButtonModel.DOWN]: #.down:
                self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal
                return True # BUTTON HAS BEEN CLICKED
            else:
                self.status = ButtonModel.STATUS[ButtonModel.HOVER] #.hover
        else:
            self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal

class ButtonView:

    def drawButton(screen, buttonSurface, position):
        screen.blit(buttonSurface, position)

    def __init__(self, screen):
        self.screen = screen

    def draw(self, rect, status):
        color = black
        if status == ButtonModel.STATUS[ButtonModel.NORMAL]: #.normal:
            color = white
        elif status == ButtonModel.STATUS[ButtonModel.DOWN]: #.down:
            color = blue
        elif status == ButtonModel.STATUS[ButtonModel.HOVER]: #.hover:
            color = green
        elif status == ButtonModel.STATUS[ButtonModel.DISABLED]: #.disabled:
            color = grey

        self.screen.fill( color, rect )
        
class ButtonMediator:

    def __init__(self, manager):
        self.buttons = []
        self.actions = []
        self.views = []
        self.actionArgs = []

        self.manager = manager
        self.manager.register(MouseEvent, self)
        self.manager.register(TickEvent, self)

    def addButton(self, rect, screen, action, actionargs):
        self.buttons.append( ButtonModel( rect ) )
        self.actions.append( action )
        self.views.append( ButtonView(screen) )
        self.actionArgs.append( actionargs )

    def disableButton(self, i):
        self.buttons[i].disable()

    def enableButton(self, i):
        self.buttons[i].enable()

    def notify(self, event):
        for i in range( len(self.buttons) ):
            b = self.buttons[i]
            if event.kind == MouseEvent.kind:
                if b.mouseAction(event.mouse):
                    f = self.actions[i]
                    f( self.actionArgs[i] )
            self.views[i].draw(b.rect, b.status)
            # redraw on mouse event only = bad bad flicker issues
            # (black when mouse not being used)
            
class MouseEvent(Event):
    kind = "mouse"
    def __init__(self, mouseevent):
        Event.__init__(self)
        self.mouse = mouseevent

class UserInterface:
    def __init__(self, manager):
        #self.listeners = {}
        self.manager = manager
        self.manager.register( TickEvent, self )

    #def add(self, kind, listener):
    #    #self.listeners[ listener ] = 1
    #    if not self.listeners.has_key(kind):
    #        self.listeners[kind] = {}
    #    self.listeners[kind][listener] = 1

    #def drop(self, kind, listener):
    #    if self.listeners.has_key(kind):
    #        if self.listeners[kind].has_key(listener):
    #            del self.listeners[ kind ][ listener ]

    def notify(self, event):
        #event = None
        #for event in pygame.event.get():
        #    for listener in self.listeners[ event.type ]:
        #        listener.notify(event)
        event = None
        for event in pygame.event.get():
            if event.type == MOUSEBUTTONUP or event.type == MOUSEBUTTONDOWN or event.type == MOUSEMOTION:
                self.manager.post( MouseEvent(event) )
            elif event.type == QUIT:
                self.manager.post( QuitEvent() )

    #def post(self, event):
    #    self.manager.post(event)

class MouseInterface:
    def __init__(self, manager):
        self.manager = manager
        self.manager.add( MOUSEBUTTONUP, self )
        self.manager.add( MOUSEBUTTONDOWN, self )
        self.manager.add( MOUSEMOTION, self )
        

    def notify(self, event):
        #event = None
        #for event in pygame.event.get():
        #    if event.type == MOUSEBUTTONUP or event.type == MOUSEBUTTONDOWN or event.type == MOUSEMOTION:
        #        self.manager.post( MouseEvent (event) )
        self.manager.post( MouseEvent (event) )
                
class QuitInterface:
    def __init__(self, manager):
        print "able to exit"
        self.manager = manager
        #self.manager.register( TickEvent, self )
        self.manager.add( QUIT, self )

    def notify(self, event):
        #print "checking for quit..."
        #event = None
        #for event in pygame.event.get():
        #    if event.type == QUIT:
        #        self.manager.post( QuitEvent( ) )
        self.manager.post( QuitEvent() )
                
class GameLogic:
    def __init__(self):
        #self.state = []
        self.state = ()

        self.logictree = {
            () : "Root", (0,) : "0", (1,) : "1", (2,) : "2",
            (0,1) : "01", (0,2) : "02", (1,0) : "10",
            (1,2) : "12", (2,0) : "20", (2,1) : "21",
            (0,1,2): "012", (0,2,1): "021",
            (1,0,2): "102", (1,2,0): "120",
            (2,0,1): "201", (2,1,0): "210"
            }

        print self.state
        print self.logictree [ tuple(self.state) ]
        

    def make_choice(self, value):
        temp = list(self.state)
        temp.append(value)
        self.state = tuple( temp )

        #self.state.append(value)

        #if tuple(self.state) in self.logictree:
        #    print self.logictree[ tuple(self.state) ]
        if self.logictree.has_key(self.state):
            print self.logictree[ self.state ] 
        else:
            print self.state
            #print tuple(self.state)
            print "INVALID LOGIC STATE"

        


def main():

    game = GameLogic()
    
    manager = Mediator()
    
    
    #mouse = MouseInterface(manager)
    user = UserInterface(manager)
    #mouse = MouseInterface(user)
    #quitter = QuitInterface(user)
    
    screen = ScreenView(manager, (400,400), black)

    buttons = ButtonMediator(manager)

    def chooseButton( value ):
        buttons.disableButton( value )
        game.make_choice( value )
    

    buttons.addButton( pygame.Rect(50,50,10,10), screen.screen, chooseButton, 0 )
    buttons.addButton( pygame.Rect(100,50,10,10), screen.screen, chooseButton, 1 )
    buttons.addButton( pygame.Rect(150,50,10,10), screen.screen, chooseButton, 2 )
    #buttons.addButton( pygame.Rect(200,50,10,10), screen.screen, chooseButton, 3 )

    controller = TickGeneratorController(manager)

    controller.run()

if __name__ == "__main__":
    main()

Labels: , , , , ,

0 comments.
Leave one.

Next up is some more recent code for buttons. Still full of obvious flaws, but a big improvement over my last try. First up are some changes to myevents.py:

I also made some changes to my directory structure. I've moved up in the world from two files (myevents.py and main.py) to four. I now have mycolors.py and commoncomponents.py. The contents of mycolors.py are nothing more than basic color definitions which I tend to reuse frequently like black = (0,0,0). The commoncomponents are the handful of classes I needed to reuse in the last attempt at buttons - ScreenView, TickGeneratorEvent, and QuitEvent. This is where things get a little more interesting. I made a change to Screen View this time around.

Screen View

The basic difference is simply that this view is more general now. Instead of hard coding the size and color, they're set in init. It may only be a few lines different - but it's a much better class for it.

class ScreenView:
    """
    Create a window which repaints everything at every tick event.
    """
    def __init__(self, manager, size, bgcolor):
        """
        Create a new window with a specified size and background color
        """
        pygame.init()
        manager.register(TickEvent, self)
        self.screen = pygame.display.set_mode( size )
        self.bg = bgcolor
        self.screen.fill(self.bg)

    def notify(self, event):
        """
        Listens to Tick Events. Repaints the screen.
        """
        pygame.display.flip()
        self.screen.fill(self.bg)

main.py

The responsibilities of each class, and how they communicate, has changed up quite a bit. Here's what each piece should do:

As you can see, I've thrown some new stuff out here. It's certainly different than before. I think it's better, but I'm not yet sure by how much. Since I have stepped outside the bounds of those guiding me, I'll be learning as I go.

I've decided to call the components that react directly to user input as interfaces (hence, mouse interface). I'm pretty sure that's a bad naming convention, since interface has its own implications in programming - but like so many other things, it's going to stay until I come up with something better. Meanwhile, notice that the button mediator (a new component in this version) does most of the work. What I've tried to encapsulate in it is the logic behind the buttons. Back to something resembling the model, view, and logic, with some events and human interfacing to tie it all together.

button model

In some terrible attempt to use a static dictionary to store the possible statuses of a button, I ended up with a garbled mess. You'll see in just a moment. The point is, I ask that you please pass it by without comment (unless you have a suggestion and I haven't already fixed it). I haven't quite decided how to get the effect I want without making it worse and harder to understand. I think I'm being confused by a vague memory of what miniscule amout of perl that I know. Anyway, buttons are delightedly simple now. As suggested above, they only do three things. Storing information is trivial. So naturally, the key is how they react to mouse events. They also have a pair of functions, enable and disable, which modify the status, since there's no logical mouse action to cause that. Notice that the button model is abstracted one level away from events - it neither listens to nor posts them.

class ButtonModel:

    # work around for now - I like the notation STATUS.normal better
    NORMAL = 'normal'
    DOWN = 'down'
    HOVER = 'hover'
    DISABLED = 'disabled'
    STATUS = { 'normal':0 , 'down':1 , 'hover':2 , 'disabled':3 }

    def __init__(self, rect):
        self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal
        self.rect = rect

    def enable(self):
        if self.status == ButtonModel.STATUS[ButtonModel.DISABLED]: #.disabled:
            self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal

    def disable(self):
        self.status = ButtonModel.STATUS[ButtonModel.DISABLED] #.disabled

The mouseAction function actually does two things. The first is update the button status. The second is to return a value that indicates when a button is clicked. First, we check if the button is disabled. If it is, there is no need to do anything else. Second, we check if the mouse position collides with the button. If it does not, the button must be in a neutral state (Normal Status). If the mouse is pressed over the button, but released elsewhere (or vise versa), the button should change status but never register a click. The initial reason was mostly visual - most buttons appear to be pressed down when holding the mouse over them. However, it's worth while to store this status in the button model for click checking. If the button has not been pressed down, it can not be clicked. If the mouse is released while over the button, and it was pressed down, it should be. This is the way that this method determines the click.

       
    def mouseAction(self, mouseevent):
        if self.status == ButtonModel.STATUS[ButtonModel.DISABLED]: return #.disabled: return

        if self.rect.collidepoint( mouseevent.pos ) :
            if mouseevent.type == MOUSEBUTTONDOWN:
                self.status = ButtonModel.STATUS[ButtonModel.DOWN] #.down
            elif mouseevent.type == MOUSEBUTTONUP and self.status == ButtonModel.STATUS[ButtonModel.DOWN]: #.down:
                self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal
                return True # BUTTON HAS BEEN CLICKED
            else:
                self.status = ButtonModel.STATUS[ButtonModel.HOVER] #.hover
        else:
            self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal

button view

It seemed like a good idea for the button view to have a static draw method. So it does. I haven't yet used it. The idea was that if, for some reason, I felt the need to store visual information about the button elsewhere, I could simply blit the button visual onto the screen, using the button view class without needing an instance. I haven't yet thought up a reason to organize the code that way, but the idea is planted and ready for use in case I need it. Anyway, what the view really does is store the screen. To draw to the screen, it requires the position/size of the button, and the status. In this simple case, it uses the status to determine what color to draw in. I initially had the draw method look like:

    def draw(self, rect, status):
        if status == ButtonModel.STATUS[ButtonModel.NORMAL]: #.normal:
            self.screen.fill( white, rect )
        elif status == ButtonModel.STATUS[ButtonModel.DOWN]: #.down:
            self.screen.fill( blue, rect )
        elif status == ButtonModel.STATUS[ButtonModel.HOVER]: #.hover:
            self.screen.fill( green, rect )
        elif status == ButtonModel.STATUS[ButtonModel.DISABLED]: #.disabled:
            self.screen.fill( grey, rect )
But now, just right this very moment, as I was about to copy and paste it here, as I am writing this, it occured to me to do this instead:
        color = black
        if status == ButtonModel.STATUS[ButtonModel.NORMAL]: #.normal:
            color = white
        elif status == ButtonModel.STATUS[ButtonModel.DOWN]: #.down:
            color = blue
        elif status == ButtonModel.STATUS[ButtonModel.HOVER]: #.hover:
            color = green
        elif status == ButtonModel.STATUS[ButtonModel.DISABLED]: #.disabled:
            color = grey

        self.screen.fill( color, rect )

button mediator

So this critter does a ton of the work. It coordinates all the pieces of information (buttons, actions, and view, etc) by keeping an array for each and using the index. Probably not the best idea, but it works well enough for now. Perhaps I will later make another tidy intermediate class, and just store and array of those. I admit I'm just thinking out loud on that. I'm also not certain if the button action should be stored as part of the model. It certainly could be. Anyway, that gets a bit off topic. In short, this mediator sends mouse events to all the buttons, to find out if the button has been clicked. If it has, it calls the button action function, with provided parameters (that's the etc). It then draws all the button views on every tick and every mouse event. Probably no need to draw on mouse events - but it definately needs to draw on tick events (otherwise the screen will go black when you stop moving the mouse). It would be wise to implement a 'dirty' flag to redraw only bits of the screen, but I'm not feeling up for the complexity of that just yet. My code is a few versions ahead of this right now, but I'm about at the point where visualization needs to ramp up to game-quality animated stuff. So patience, patience.

class ButtonMediator:

    def __init__(self, manager):
        self.buttons = []
        self.actions = []
        self.views = []
        self.actionArgs = []

        self.manager = manager
        self.manager.register(MouseEvent, self)
        self.manager.register(TickEvent, self)

    def addButton(self, rect, screen, action, actionargs):
        self.buttons.append( ButtonModel( rect ) )
        self.actions.append( action )
        self.views.append( ButtonView(screen) )
        self.actionArgs.append( actionargs )

    def disableButton(self, i):
        self.buttons[i].disable()

    def enableButton(self, i):
        self.buttons[i].enable()

    def notify(self, event):
        for i in range( len(self.buttons) ):
            b = self.buttons[i]
            if event.kind == MouseEvent.kind:
                if b.mouseAction(event.mouse):
                    f = self.actions[i]
                    f( self.actionArgs[i] )
            self.views[i].draw(b.rect, b.status)

Glue

I'll skip over mouse events and interfaces. Like normal, they'll be included in the monster spewing post of full code at the end (which is immenent now). To make this all come together, we need to do more than just create the button mediator in the main function. We need to add some buttons to it. The rect and screen parts are easy - we've provided things like that before. The tricky bit is the button actions. I've forgotton if I mentioned where I'm aiming with this (in the short term), but long story short, I only want each button to be used once. So, quite simply, clicking on a button disables it. The button action arguments are the index of the button in question. It may sound a bit confusing, but in the next post or two things will become a bit less unclear. They may still be muddy, I make no promises.

def main():
    manager = Mediator()

    mouse = MouseInterface(manager)
    screen = ScreenView(manager, (400,400), black)

    buttons = ButtonMediator(manager)
    # def addButton(self, rect, screen, action, actionargs):
    buttons.addButton( pygame.Rect(50,50,10,10), screen.screen, buttons.disableButton, 0 )
    buttons.addButton( pygame.Rect(100,50,10,10), screen.screen, buttons.disableButton, 1 )
    buttons.addButton( pygame.Rect(150,50,10,10), screen.screen, buttons.disableButton, 2 )
    buttons.addButton( pygame.Rect(200,50,10,10), screen.screen, buttons.disableButton, 3 )

    controller = TickGeneratorController(manager)

    controller.run()

As a final note, I'd like to point out that you must control + C this program to quit. I've provided nothing, so clicking on the 'x' in the window doesn't do a thing. That'll be fixed fairly soon (in the next two posts, if not the next one. I can't remember which I fixed this in.)

mycolors.py

black = (0,0,0)
white = (255,255,255)
red = (255,0,0)
blue = (0,0,255)
green = (0,255,0)
grey = (100,100,100)
gray = grey

commoncomponents.py

from myevents import *
import pygame

class QuitEvent(Event):
    kind = "quit"

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

class ScreenView:
    """
    Create a window which repaints everything at every tick event.
    """
    def __init__(self, manager, size, bgcolor):
        """
        Create a new window with a specified size and background color
        """
        pygame.init()
        manager.register(TickEvent, self)
        self.screen = pygame.display.set_mode( size )
        self.bg = bgcolor
        self.screen.fill(self.bg)

    def notify(self, event):
        """
        Listens to Tick Events. Repaints the screen.
        """
        pygame.display.flip()
        self.screen.fill(self.bg)

            
class TickGeneratorController:
    """
    Generates Tick Events at a rate of 30 FPS.
    """
    FPS = 30
    
    def __init__(self, manager):
        """
        Create a source for regular timer events.
        """
        self.manager = manager
        self.manager.register(QuitEvent, self)

        self.running =  True

        self.clock = pygame.time.Clock()

    def run(self):
        """
        Begin generating tick events at a rate of 30 per second.
        Continue until a Quit Event is heard.
        """
        while self.running:
            self.manager.post( TickEvent(pygame.time.get_ticks()) )
            self.clock.tick(self.FPS) # throttle back the CPU

    def notify(self, event):
        """
        Listen for quit events. If one is received with a quit = true value,
        Stop from running.
        """
        if self.running:
            if event[QuitEvent.kind]: # if it is set to false - keep running
                self.running = False

myevents.py

from weakref import WeakKeyDictionary

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.sync = True
        self.generic = None # just in case

    def __repr__(self):
        """Custom representation for the event."""
        string = self.kind + " Event" # should be doing a string manipulation to capitalize first letter
        for attr in self.__dict__.keys():
            if attr[:2] == '__':
                string += "\n\tattr %s=" %attr
            elif attr != "kind": # should be the same as just "else"
                string += "\n\tattr %s=%s" % (attr, self.__dict__[attr])
                
        return string

    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


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):
        """Set the name and tick properties."""
        Event.__init__(self)
        self.tick = time

class EventManager:
    """
    A manager for coordinating communication. A glorified event listener.
    """

    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):
        """Post an event to all the registered listeners"""
        for listener in self.listeners:
            listener.notify(event)

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

    def register(self, clazz, listener):
        """Register a listener for a particular class of event"""
        self.add(clazz)
        self.managers[clazz.kind].register(listener)

    def add(self, clazz):
        """Add a manager for a class of event explicitly."""
        if not clazz.kind in self.managers:
            self.managers[clazz.kind] = EventManager()
            
    def drop(self, clazz):
        """Drop a manager for a class of event."""
        self.managers[clazz.kind] = None

    def registerAll(self, listener):
        """
        Register an event to listen to all current classes of events.
        Note: does nothing with event classes which have not yet been added as listeners
        (either indirectly by registering a listener to it, or directly with
        the add method.
        """
        for kind in self.managers:
            self.managers[kind].register(listener)

    def unregister(self, clazz, listener):
        """Unregister a listener for a particular classes of events."""
        if clazz.kind in self.managers:
            self.managers[clazz.kind].unregister(listener)

    def unregisterAll(self, listener):
        """Unregister a listener for all classes of events."""
        for kind in self.managers:
            self.managers[kind].unregister(listener)

    def post(self, event):
        """
        Post an event to all interested listeners. Note that only special
        events with a sync value of False are posted immediately to listeners.
        All other events are posted in bunches, marked by a Tick Event.
        """
        if event.kind == TickEvent.kind:
            self.events.append(event)
            temp = self.events
            self.events = []
            
            for e in temp:
                if e.kind in self.managers:
                    self.managers[e.kind].post(e)
        else:
            if event.sync:
                self.events.append(event)
            else:
                if event.kind in self.managers:
                    self.managers[event.kind].post(event)

main.py

from commoncomponents import *
from myevents import *
from pygame.locals import *
from mycolors import *
import pygame


class ButtonModel:

    # work around for now - I like the notation STATUS.normal better
    NORMAL = 'normal'
    DOWN = 'down'
    HOVER = 'hover'
    DISABLED = 'disabled'
    STATUS = { 'normal':0 , 'down':1 , 'hover':2 , 'disabled':3 }

    def __init__(self, rect):
        self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal
        self.rect = rect

    def enable(self):
        if self.status == ButtonModel.STATUS[ButtonModel.DISABLED]: #.disabled:
            self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal

    def disable(self):
        self.status = ButtonModel.STATUS[ButtonModel.DISABLED] #.disabled
        
    def mouseAction(self, mouseevent):
        if self.status == ButtonModel.STATUS[ButtonModel.DISABLED]: return #.disabled: return

        if self.rect.collidepoint( mouseevent.pos ) :
            if mouseevent.type == MOUSEBUTTONDOWN:
                self.status = ButtonModel.STATUS[ButtonModel.DOWN] #.down
            elif mouseevent.type == MOUSEBUTTONUP and self.status == ButtonModel.STATUS[ButtonModel.DOWN]: #.down:
                self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal
                return True # BUTTON HAS BEEN CLICKED
            else:
                self.status = ButtonModel.STATUS[ButtonModel.HOVER] #.hover
        else:
            self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal

class ButtonView:

    def drawButton(screen, buttonSurface, position):
        screen.blit(buttonSurface, position)

    def __init__(self, screen):
        self.screen = screen

    def draw(self, rect, status):
        #if status == ButtonModel.STATUS[ButtonModel.NORMAL]: #.normal:
        #    self.screen.fill( white, rect )
        #elif status == ButtonModel.STATUS[ButtonModel.DOWN]: #.down:
        #    self.screen.fill( blue, rect )
        #elif status == ButtonModel.STATUS[ButtonModel.HOVER]: #.hover:
        #    self.screen.fill( green, rect )
        #elif status == ButtonModel.STATUS[ButtonModel.DISABLED]: #.disabled:
        #    self.screen.fill( grey, rect )

        color = black
        if status == ButtonModel.STATUS[ButtonModel.NORMAL]: #.normal:
            color = white
        elif status == ButtonModel.STATUS[ButtonModel.DOWN]: #.down:
            color = blue
        elif status == ButtonModel.STATUS[ButtonModel.HOVER]: #.hover:
            color = green
        elif status == ButtonModel.STATUS[ButtonModel.DISABLED]: #.disabled:
            color = grey

        self.screen.fill( color, rect )

        
class ButtonMediator:

    def __init__(self, manager):
        self.buttons = []
        self.actions = []
        self.views = []
        self.actionArgs = []

        self.manager = manager
        self.manager.register(MouseEvent, self)
        self.manager.register(TickEvent, self)

    def addButton(self, rect, screen, action, actionargs):
        self.buttons.append( ButtonModel( rect ) )
        self.actions.append( action )
        self.views.append( ButtonView(screen) )
        self.actionArgs.append( actionargs )

    def disableButton(self, i):
        self.buttons[i].disable()

    def enableButton(self, i):
        self.buttons[i].enable()

    def notify(self, event):
        for i in range( len(self.buttons) ):
            b = self.buttons[i]
            if event.kind == MouseEvent.kind:
                if b.mouseAction(event.mouse):
                    f = self.actions[i]
                    f( self.actionArgs[i] )
            self.views[i].draw(b.rect, b.status)
            # redraw on mouse event only = bad bad flicker issues
            # (black when mouse not being used)
            
class MouseEvent(Event):
    kind = "mouse"
    def __init__(self, mouseevent):
        Event.__init__(self)
        self.mouse = mouseevent

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

    def notify(self, event):
        event = None
        for event in pygame.event.get():
            if event.type == MOUSEBUTTONUP or event.type == MOUSEBUTTONDOWN or event.type == MOUSEMOTION:
                self.manager.post( MouseEvent (event) )

def main():
    manager = Mediator()

    mouse = MouseInterface(manager)
    screen = ScreenView(manager, (400,400), black)

    buttons = ButtonMediator(manager)
    # def addButton(self, rect, screen, action, actionargs):
    buttons.addButton( pygame.Rect(50,50,10,10), screen.screen, buttons.disableButton, 0 )
    buttons.addButton( pygame.Rect(100,50,10,10), screen.screen, buttons.disableButton, 1 )
    buttons.addButton( pygame.Rect(150,50,10,10), screen.screen, buttons.disableButton, 2 )
    buttons.addButton( pygame.Rect(200,50,10,10), screen.screen, buttons.disableButton, 3 )

    controller = TickGeneratorController(manager)

    controller.run()

if __name__ == "__main__":
    main()

Labels: , , ,

0 comments.
Leave one.

Lots of computer games use buttons. An "old school" RPG (ie: pixel sprites and all), probably doesn't need mouse interaction, but I plan on making other game types too. And buttons are very very (ok, 100%) likely to be used. I know this because I've coded up a few new itterations already, one of which is getting surprisingly close to becoming a game. But I'll get to that later. For now - BUTTONS!

Buttons!

I'd like to appologize in advance, I wrote this code a couple of days ago, so I may forget to mention some of my alterations. I'll post the entire code though, so you should be able to find anything I miss. Although, so far so good, because it looks like the only change to myevents.py was the name of the manager class. I changed it to Mediator. If you check out my last post on design patterns, you'll find I have absolutely no clue if this is accurate or appropriate, but at least I don't have the confusion of "Manager" and "EventManager". However, since I see no reason to change a ton of code for just a name, the instance of the class is still "manager", as you will see.

I thought a bit about what buttons usually do, and came up with a list of possible statuses they could have. As you will see, in trying to keep with the same methodology as before, it becomes very easy to make this stuff more confusing than it needs to be. There will be a number of events to handle mouse activities. I decided to describe the button changes by noting the button status in the Button Event so that the view could react to all the possibilities. I came up with this list:

I also need to have buttons and views know something about each other, since there can be more than one button. I decided to give each button an ID, and to inform the view of the id. The ID is the primary data in the Button Event (stored in the button attribute). So the Button Event is built much like every other event I've written so far, but it also has a static list CONSTANTS. In some very poor coding, this is a hash (dictionary) with the status name as both the key and value. I was probably trying to be clever and just being stupid instead. Please let it slide.

I added three events to handle mouse actions. Mouse button presses, releases and mouse movement each have an event. These are pretty standard.

The Button Model listens to all the mouse events. The only information it stores is ID, rect (position and size), and a boolean called ready. Again, this code is mediocre, IMO, but I didn't fix it up since I recognized that I'd be moving in a different direction. All the meat takes place in the notify method. The notify method handles the different mouse events to prepare for a button click. It is smart enough not to click if the user presses and holds the button, moves the mouse off, then releases. When the user clicks down on the button, the ready attribute is flagged to true, indicating that releasing the mouse button while still inside the button area means it should activate the button. Most of the other status possibilities are not even realized in this program - I abandoned it for a new version before implementing them. Anyway, the buttons send off a ButtonEvent, with the button attribute set to the button id, and the attribute "click" to true.

The Button View listens to both tick and button events. It requires a screen, button ID, and a rect to draw in. The color of the button is static since there's no reason to add that complexity yet. This view must listen to Tick Events or the button will not be drawn to the screen most of the time (remember that the ScreenView clears the screen with black every frame). So on tick events, the button is drawn to the screen (it does not show any changes when clicking or hovering over the button - again I hadn't gotten that far when I realized I didn't like where this was leading - but you will see this in an upcoming post). On button events, a message is printed to the console. Note that it doesn't even check what button constant was set. The whole process was feeling very manual and hard to use. There was no general way to make use of the extra information.

Finally, there is the Mouse Controller. This handy dandy listener goes through the pygame events every tick, and sends out the various mouse events. It's pretty straightforward, and hard to avoid needing this one.

That's it for new classes. As far as the old ones go - we'll only need a few. It's kind of worthwhile to note which classes are the most reusable. I left out all the old classes except for Screen View, Tick Generator Controller, and Quit Event. Notice that I never got around to using Quit Event in this version, but it's still an obviously useful event, so I left it in.

The main function is pretty simple, we create a Mediator named manager, a mouse controller, screen view, and a pair of button models and views. Then create and run the tick generator. The result is a black screen with two white buttons drawn on it. The visual is very very static (despite being redrawn every frame, since we never change it), but when you click on the top button the text "Button 1 has been clicked." in the console. The lower button says "Button 2 has been clicked.".

code

I'm including myevents.py for posterity - even though it is almost exactly the same as before.

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 Mediator:
    """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)
     

And here's the main shebang.

import pygame
from myevents import *
from pygame.locals import *


class ButtonEvent(Event):
    kind = "button"

    CONSTANT = {
                'HOVER':'hover',
                'CANCEL':'cancel',
                'CLICK':'click',
                'PRESS':'press',
                'RELEASE':'release',
                'ENABLE':'enable'
                }

    def __init__(self, ID):
        Event.__init__(self)
        self.button = ID

    #def __setitem__(self, key, value):
    #    #self.key = value
    #    self.__dict__[key] = value

class MouseMotionEvent(Event):
    kind = "mousemotion"
    def __init__(self, event):
        Event.__init__(self)
        self.name = "Mouse Motion"
        self.mousemotion = event
class MouseButtonDownEvent(Event):
    kind = "mousedown"
    def __init__(self, event):
        Event.__init__(self)
        self.name = "Mouse Button Down"
        self.mousedown = event
class MouseButtonUpEvent(Event):
    kind = "mouseup"
    def __init__(self, event):
        Event.__init__(self)
        self.name = "Mouse Button Up"
        self.mouseup = event

class ButtonModel(Listener):
    def __init__(self, manager, ID, rect):
        self.id = ID
        self.manager = manager
        manager.register(MouseMotionEvent, self)
        manager.register(MouseButtonDownEvent, self)
        manager.register(MouseButtonUpEvent, self)
        #self.rect = rect
        self.rect = pygame.Rect( rect )
        self.ready = False

    def notify(self, event):
        if event.kind == MouseButtonUpEvent.kind:
            if self.rect.collidepoint( event.mouseup.pos ) and self.ready:
                #self.manager.post( ButtonEvent(self.id) )
                e = ButtonEvent(self.id)
                e[ ButtonEvent.CONSTANT.get('CLICK') ] = True

            self.ready = False
            
        if event.kind == MouseButtonDownEvent.kind:
            if self.rect.collidepoint( event.mousedown.pos ):
                #self.manager.post( ButtonEvent(self.id) )
                self.ready = True

        if event.kind == MouseMotionEvent.kind:
            if self.rect.collidepoint( event.mousemotion.pos ):
                #print "Hovering over button "+str(self.id)
                pass

class ButtonView(Listener):
    def __init__(self, manager, screen, ID, rect):
        manager.register(ButtonEvent, self)
        manager.register(TickEvent, self)
        self.screen = screen
        self.rect = rect
        #self.rect = pygame.Rect( rect )
        self.id = ID
        
    def notify(self, event):
        if event.kind == TickEvent.kind:
            self.screen.fill( (255,255,255), self.rect)
        elif event.kind == ButtonEvent.kind and self.id == event.button:
            print "Button " +str(self.id)+ " has been clicked."


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

    def notify(self, event):
        event = None
        for event in pygame.event.get():
            if event.type == MOUSEBUTTONUP:
                self.manager.post( MouseButtonUpEvent(event) )
            if event.type == MOUSEBUTTONDOWN:
                self.manager.post( MouseButtonDownEvent(event) )
            if event.type == MOUSEMOTION:
                self.manager.post( MouseMotionEvent(event) )


# snagged from previous examples

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

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



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

def main():
    manager = Mediator()

    mouse = MouseController(manager)
    screen = ScreenView(manager)
    button1model = ButtonModel( manager, 1, ( (50,50), (50,50) ) )
    button1view = ButtonView( manager, screen.screen, button1model.id, button1model.rect )

    button2model = ButtonModel( manager, 2, ( (50, 110), (50,50 ) ) )
    button2view = ButtonView( manager, screen.screen, button2model.id, button2model.rect )


    controller = TickGeneratorController(manager)

    controller.run()

if __name__ == "__main__":
    main()
    

Labels: , , ,

0 comments.
Leave one.

Since my last post, I've been tormented by some mounting problems with this code. It's all decent enough, but I keep coming up with more ideas - stuff I'd like to add. Every time I come up with a new feature, the complication of the code seems to multiply. My brain keeps screaming at me that I'm doing something wrong - there's clearly something off in the way I'm implementing this design pattern. On the topic of design patterns, I'm not entirely sure which this falls into. I'm fairly new to the details of design patterns, but this must fall somewhere in the MVC/Mediator/Observer model. I suspect it's mostly observer, and that's the cause of the complications I foresee, but I'm not entirely certain.

Anyway, the problem I see is this: Games are complex. RPGs are particularly complicated, and that is my ultimate goal. To continue on my current path, it is almost impossible not to expect about 1 million events. ONE MILLION almost IDENTICAL classes. That sounds like bad design if I've ever heard of it. I've wracked my brain all week, and finally feel like I'm starting to stumble my way over this hurdle. Like so many things, there's tons of information on basic design patterns, but much much less (and far less useful) information on implementing the patterns in nontrivial examples.

So my next few posts will build up into a new direction. I'll probably stop tagging the posts with MVC, since I'm not at all sure of its accuracy. Don't worry though, I'll keep up the running list so we can follow this path without hurting ourselves. We'll now be going on a wild adventure whos outcome is uncertain, starting with some simple things like buttons, and then throwing together some simple game logic and views.

Labels: , , , ,

0 comments.
Leave one.

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.

I've decided to expand - I'll be making multiple event types. Moreover, there will be multiple Event Managers to prevent objects from being flooded with events they don't care about.

The best way, I concluded, to handle multiple Event Managers was to have a Manager for that. That way objects will still only need to interact with one class, using (virtually) the same interface. Then I won't have to recode much and there will be rainbows and unicorns. Or something. There will certainly be something. *Ah-hem*. I've called the new class "Manager". It's so uncreative and nondescript that I'm sure it'll have to change. Something better than Manager, that isn't "EventManagerManager" either (because reading that a year from now I'll have less than no idea what it means). Things Manager needs to do:

Events

First things first, the Event class will be gaining a new feature - type. Since 'type' is a keyword, I'll be using 'kind' instead. This is not kind as in "kind or mean?", but as a synonym for type (like "what type is it?"). Since I plan on subclassing Event, this will be a static property, done like this:

class Event:
    kind = "generic"
    
    def __init__(self):
        ...
In all other ways, Event is the same. But there will be some additional classes needed. I'll start with Tick Event. This type of event will be defined in myevents.py, while the others will be defined in main.py. The reason is that Tick Event will be used (indirectly) in the logic for Manager. This way, the only specific event type is provided in the module with the other event classes. This is not entirely necessary, since the manager won't require the events it recieves to be related to the Event class at all - they only need to act like it. This is why I'm using the property kind, so that I don't have to test if I have an instance of any event class.

Tick Event:

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
Although I haven't shown it yet, there's a reason I named the kind "tick". This is because the only property of a Tick Event that cannot be found in Event is the "tick" property. I will use the same convention for all my new event types.

Event Managers

The Event Manager class is going to be changning a fair bit too. It's actually going to revert and become simpler. A large part of the reason for this is to preserve the order of events. Otherwise, all the events of a particular type (such as Fibonacci events) will be posted at once by Event Manager, followed by all the events of the next type. Much of the logic currently in Event Manager will be transfered to Manager. So, like Event Manager before it, Manager will need a list of events. It will also need some Event Managers. Since I plan on posting to Event Managers based on kind, I chose to use a dictionary, keyed by kind.

I'll just go ahead and show the whole class at once, as well as the modified Event Manager.

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):
        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, kind, listener):
        self.add(kind)
        self.managers[kind].register(listener)

    def add(self, kind):
        if not kind in self.managers:
            self.managers[kind] = EventManager()
            
    def drop(self, kind):
        self.managers[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, kind, listener):
        if kind in self.managers:
            self.managers[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)
        

Other

Another small change to the myevents.py file is to Listeners. This is a real quick one line change - in the init function, use registerAll instead of register.

We'll finish up with some changes to the test code at the bottom, then move on to main.py.

    e = TickEvent()
    print e[TickEvent.kind]
    e = TickEvent('hi')
    print e[TickEvent.kind]

    print e['anotherAttribute']
I'm going to skip the explaination on this bit, as it isn't really important.

main.py

I'll try to skip going into too much detail (I have more code to write after I finish this post) about little changes like from myevents import * and just post the full code (again). A brief summary of changes: Introduce QuitEvent, FibonacciEvent, minor changes to TickGenerator, and general use of Manager over Event Manager.

New Event Types

The new Events are pretty similar in form to the ones previously defined - they set a kind, name, sync and 'kind' property (such as the tick property for events of kind 'tick'). The quit event is exactly what you'd expect. As before, it is the only event with a sync value of False so that there is no delay in it's application. You can take a peak at those from the full code, as I can see no reason to post excess code chunks.

Changes to Listeners

Our implemented listeners (like Tick Generator Controller) have changed to adapt to the new Manager. The most notable change in Tick Generator is in init and notify. In init, you will see that we now register to a specific kind of event. This controller only listens to Quit Events - other events are of no interest. This means that in notify, we no longer need to check if the event has a 'quit' property - it will. For some other listeners, this will not be possible because they'll need to hear more than one event type.

For the Quit Controller, the listener type must be set, since it can be more than one thing. Once constructed, a particular instance of this controller only listens to one event type, so its notify method is similar to Tick Generator.

The Event Logger still listens to everything - notice that I didn't use __init__. Due to the change I made in Listener, it will try to listen to all events (registerAll). However there is a flaw - there's the possibility that some of the event types will not be in the Manager when this is called. This is why the Manager also has an add method, which I'll use in the main function.

The Fibonacci Model also listens to only one type of event. This one is only interested in Tick Events. So, in short, none of the listeners have to use type checking in their notify functions. The only listener that hears more than one kind of event only prints them out, using methods available to all event types through the parent class.

main function

The main function has changed surprisingly little. After creating manager (an instance of Manager, instead of EventManager), we add the events explicitly. Again, this is so we can be absolutely sure the Event Logger hears all event types. Notice also how similar all the listeners are to their previous constructions - most are exactly the same. The results of the initializations are much different, but the method calls to create our instances are not. Neat.

code (myevents)

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

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):
        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, kind, listener):
        self.add(kind)
        self.managers[kind].register(listener)

    def add(self, kind):
        if not kind in self.managers:
            self.managers[kind] = EventManager()
            
    def drop(self, kind):
        self.managers[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, kind, listener):
        if kind in self.managers:
            self.managers[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)
        
            

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

"""Test the contents of this file."""
if __name__ == "__main__":

    e = Event()
    print e
    #print e.tick # still errors

    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)

    e = TickEvent()
    print e[TickEvent.kind]
    e = TickEvent('hi')
    print e[TickEvent.kind]

    print e['anotherAttribute']
    

code (main)

import pygame
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 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 = 30
    def __init__(self, manager):
        self.manager = manager # we will be posting events
        self.manager.register(QuitEvent.kind, 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, goalAttribute, goalValue):
        self.manager = manager
        self.manager.register(goalAttribute, self)

        self.goalAttribute = goalAttribute
        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.kind, 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) )

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

    stop1 = QuitController(manager, TickEvent.kind , TickGeneratorController.FPS*50) # 30 is FPS
    stop2 = QuitController(manager, FibonacciEvent.kind, 1000)

    controller.run()

if __name__ == "__main__":
    main()
    
        

Labels: , , , ,

0 comments.
Leave one.