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

Post a Comment