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.

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

Post a Comment