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.

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.
Comments

Post a Comment