I am working on a little card-swapping world-travel game that I sort of envision as a cross between Bejeweled and the 10 Days geography board games. So far the coding has been going okay, but the frame rate is pretty bad... currently I'm getting low 20's on my Core 2 Duo. This is a problem since I'm creating the game for Intel's March developer competition, which is squarely aimed at netbooks packing underpowered Atom processors.
Here's a screen from the game:
![www.necessarygames.com/my_games/betraveled/betraveled-fps.png][1]
I am very new to Python and Pygame (this is the first thing I've used them for), and am sadly lacking in formal CS training... which is to say that I think there are probably A LOT of bad practices going on in my code, and A LOT that could be optimized. If some of you older Python hands wouldn't mind taking a look at my code and seeing if you can't find any obvious areas for optimization, I would be extremely grateful. 
You can download the full source code here:
http://www.necessarygames.com/my_games/betraveled/betraveled_src0328.zip
Compiled exe here:
www.necessarygames.com/my_games/betraveled/betraveled_src0328.zip
One thing I am concerned about is my event manager, which I feel may have some performance wholes in it, and another thing is my rendering... I'm pretty much just blitting everything to the screen all the time (see the render routines in my game_components.py below); I recently found out that you should only update the areas of the screen that have changed, but I'm still foggy on how that accomplished exactly... could this be a huge performance issue?
Any thoughts are much appreciated! As usual, I'm happy to "tip" you for your time and energy via PayPal.
Jordan
Here are some bits of the source:
Main.py
#Remote imports
import pygame
from pygame.locals import *
#Local imports
import config
import rooms
from event_manager import *
from events import *
class RoomController(object):
    """Controls which room is currently active (eg Title Screen)"""
    def __init__(self, screen, ev_manager):
        self.room = None
        self.screen = screen
        self.ev_manager = ev_manager
        self.ev_manager.register_listener(self)
        self.room = self.set_room(config.room)
    def set_room(self, room_const):
        #Unregister old room from ev_manager
        if self.room:
            self.room.ev_manager.unregister_listener(self.room)
            self.room = None
        #Set new room based on const
        if room_const == config.TITLE_SCREEN:
            return rooms.TitleScreen(self.screen, self.ev_manager)
        elif room_const == config.GAME_MODE_ROOM:
            return rooms.GameModeRoom(self.screen, self.ev_manager)        
        elif room_const == config.GAME_ROOM:
            return rooms.GameRoom(self.screen, self.ev_manager)
        elif room_const == config.HIGH_SCORES_ROOM:
            return rooms.HighScoresRoom(self.screen, self.ev_manager)
    def notify(self, event):
        if isinstance(event, ChangeRoomRequest):
            if event.game_mode:
                config.game_mode = event.game_mode            
            self.room = self.set_room(event.new_room)
    def render(self, surface):
        self.room.render(surface)
#Run game 
def main():
    pygame.init()
    screen = pygame.display.set_mode(config.screen_size)
    ev_manager = EventManager()
    spinner = CPUSpinnerController(ev_manager)
    room_controller = RoomController(screen, ev_manager)    
    pygame_event_controller = PyGameEventController(ev_manager)
    spinner.run()
# this runs the main function if this script is called to run.
#  If it is imported as a module, we don't run the main function.
if __name__ == "__main__": 
    main()
event_manager.py
#Remote imports
import pygame
from pygame.locals import *
#Local imports
import config
from events import *
def debug( msg ):
    print "Debug Message: " + str(msg)
class EventManager:
    #This object is responsible for coordinating most communication
    #between the Model, View, and Controller.
    def __init__(self):
        from weakref import WeakKeyDictionary
        self.listeners = WeakKeyDictionary()
        self.eventQueue= []
        self.gui_app = None
    #----------------------------------------------------------------------
    def register_listener(self, listener):
        self.listeners[listener] = 1
    #----------------------------------------------------------------------
    def unregister_listener(self, listener):
        if listener in self.listeners:
            del self.listeners[listener]
    #----------------------------------------------------------------------
    def post(self, event):
        if  isinstance(event, MouseButtonLeftEvent):
            debug(event.name)
        #NOTE: copying the list like this before iterating over it, EVERY tick, is highly inefficient,
        #but currently has to be done because of how new listeners are added to the queue while it is running
        #(eg when popping cards from a deck). Should be changed. See: http://dr0id.homepage.bluewin.ch/pygame_tutorial08.html
        #and search for "Watch the iteration"
        for listener in list(self.listeners):
            #NOTE: If the weakref has died, it will be 
            #automatically removed, so we don't have 
            #to worry about it.
            listener.notify(event)
#------------------------------------------------------------------------------
class PyGameEventController:
    """..."""
    def __init__(self, ev_manager):
        self.ev_manager = ev_manager
        self.ev_manager.register_listener(self) 
        self.input_freeze = False
    #----------------------------------------------------------------------
    def notify(self, incoming_event):
        if isinstance(incoming_event, UserInputFreeze):
            self.input_freeze = True
        elif isinstance(incoming_event, UserInputUnFreeze):
            self.input_freeze = False        
        elif isinstance(incoming_event, TickEvent):
            #Share some time with other processes, so we don't hog the cpu
            pygame.time.wait(5)
            #Handle Pygame Events
            for event in pygame.event.get():
                #If this event manager has an associated PGU GUI app, notify it of the event
                if self.ev_manager.gui_app:
                    self.ev_manager.gui_app.event(event)
                #Standard event handling for everything else
                ev = None
                if event.type == QUIT:
                    ev = QuitEvent()
                elif event.type == pygame.MOUSEBUTTONDOWN and not self.input_freeze:
                    if event.button == 1:    #Button 1
                        pos = pygame.mouse.get_pos()
                        ev = MouseButtonLeftEvent(pos)
                elif event.type == pygame.MOUSEMOTION:
                        pos = pygame.mouse.get_pos()
                        ev = MouseMoveEvent(pos)
                #Post event to event manager
                if ev:
                    self.ev_manager.post(ev)                          
#------------------------------------------------------------------------------            
class CPUSpinnerController:
    def __init__(self, ev_manager):
        self.ev_manager = ev_manager
        self.ev_manager.register_listener(self)
        self.clock = pygame.time.Clock()
        self.cumu_time = 0
        self.keep_going = True
    #----------------------------------------------------------------------
    def run(self):
        if not self.keep_going:
            raise Exception('dead spinner')        
        while self.keep_going: 
            time_passed = self.clock.tick()
            fps = self.clock.get_fps()
            self.cumu_time += time_passed
            self.ev_manager.post(TickEvent(time_passed, fps))
            if self.cumu_time >= 1000:
                self.cumu_time = 0
                self.ev_manager.post(SecondEvent())
        pygame.quit()
    #----------------------------------------------------------------------
    def notify(self, event):
        if isinstance(event, QuitEvent):
            #this will stop the while loop from running
            self.keep_going = False            
rooms.py
#Remote imports
import pygame
#Local imports
import config
import continents
from game_components import *
from my_gui import *
from pgu import high
class Room(object):
    def __init__(self, screen, ev_manager):
        self.screen = screen
        self.ev_manager = ev_manager
        self.ev_manager.register_listener(self)
    def notify(self, event):
        if isinstance(event, TickEvent): 
            pygame.display.set_caption('FPS: ' + str(int(event.fps)))         
            self.render(self.screen) 
            pygame.display.update()
    def get_highs_table(self):
        fname = 'high_scores.txt'
        highs_table = None
        config.all_highs = high.Highs(fname)
        if config.game_mode == config.TIME_CHALLENGE:
            if config.difficulty == config.EASY:
                highs_table = config.all_highs['time_challenge_easy']
            if config.difficulty == config.MED_DIF:
                highs_table = config.all_highs['time_challenge_med']
            if config.difficulty == config.HARD:
                highs_table = config.all_highs['time_challenge_hard']
            if config.difficulty == config.SUPER:
                highs_table = config.all_highs['time_challenge_super']                  
        elif config.game_mode == config.PLAN_AHEAD:
            pass     
        return highs_table
class TitleScreen(Room):
    def __init__(self, screen, ev_manager):
        Room.__init__(self, screen, ev_manager)
        self.background = pygame.image.load('assets/images/interface/background.jpg').convert()      
        #Initialize
        #---------------------------------------
        self.gui_form = gui.Form()
        self.gui_app = gui.App(config.gui_theme)
        self.ev_manager.gui_app = self.gui_app
        c = gui.Container(align=0,valign=0)        
        #Quit Button
        #---------------------------------------
        b = StartGameButton(ev_manager=self.ev_manager)
        c.add(b, 0, 0)    
        self.gui_app.init(c)
    def render(self, surface):
        surface.blit(self.background, (0, 0))
        #GUI
        self.gui_app.paint(surface)            
class GameModeRoom(Room):
    def __init__(self, screen, ev_manager):
        Room.__init__(self, screen, ev_manager)
        self.background = pygame.image.load('assets/images/interface/background.jpg').convert()      
        self.create_gui()
    #Create pgu gui elements
    def create_gui(self):
        #Setup
        #---------------------------------------
        self.gui_form = gui.Form()
        self.gui_app = gui.App(config.gui_theme)
        self.ev_manager.gui_app = self.gui_app
        c = gui.Container(align=0,valign=-1)        
        #Mode Relaxed Button
        #---------------------------------------
        b = GameModeRelaxedButton(ev_manager=self.ev_manager)
        self.b = b
        print b.rect
        c.add(b, 0, 200)    
        #Mode Time Challenge Button
        #---------------------------------------
        b = TimeChallengeButton(ev_manager=self.ev_manager)
        self.b = b
        print b.rect
        c.add(b, 0, 250)    
        #Mode Think Ahead Button
        #---------------------------------------
#        b = PlanAheadButton(ev_manager=self.ev_manager)
#        self.b = b
#        print b.rect
#        c.add(b, 0, 300)            
        #Initialize
        #---------------------------------------       
        self.gui_app.init(c)          
    def render(self, surface):
        surface.blit(self.background, (0, 0))
        #GUI
        self.gui_app.paint(surface)    
class GameRoom(Room):
    def __init__(self, screen, ev_manager):
        Room.__init__(self, screen, ev_manager)
        #Game mode
        #---------------------------------------   
        self.new_board_timer = None        
        self.game_mode = config.game_mode
        config.current_highs = self.get_highs_table()
        self.highs_dialog = None
        self.game_over = False
        #Images
        #---------------------------------------   
        self.background = pygame.image.load('assets/images/interface/game screen2-1.jpg').convert() 
        self.logo = pygame.image.load('assets/images/interface/logo_small.png').convert_alpha() 
        self.game_over_text = pygame.image.load('assets/images/interface/text_game_over.png').convert_alpha()
        self.trip_complete_text = pygame.image.load('assets/images/interface/text_trip_complete.png').convert_alpha()
        self.zoom_game_over = None
        self.zoom_trip_complete = None
        self.fade_out = None
        #Text
        #---------------------------------------   
        self.font = pygame.font.Font(config.font_sans, config.interface_font_size)
        #Create game components   
        #---------------------------------------      
        self.continent = self.set_continent(config.continent)
        self.board = Board(config.board_size, self.ev_manager)
        self.deck = Deck(self.ev_manager, self.continent)
        self.map = Map(self.continent)
        self.longest_trip = 0
        #Set pos of game components
        #---------------------------------------   
        board_pos = (SCREEN_MARGIN[0], 109)
        self.board.set_pos(board_pos)
        map_pos = (config.screen_size[0] - self.map.size[0] - SCREEN_MARGIN[0], 57);
        self.map.set_pos(map_pos)        
        #Trackers  
        #---------------------------------------              
        self.game_clock = Chrono(self.ev_manager)
        self.swap_counter = 0
        self.level = 0    
        #Create gui
        #---------------------------------------   
        self.create_gui()
        #Create initial board
        #---------------------------------------   
        self.new_board = self.deck.deal_new_board(self.board)
        self.ev_manager.post(NewBoardComplete(self.new_board)) 
    def set_continent(self, continent_const):
        #Set continent based on const
        if continent_const == config.EUROPE:
            return continents.Europe()
        if continent_const == config.AFRICA:
            return continents.Africa()        
        else:
            raise Exception('Continent constant not recognized')     
    #Create pgu gui elements
    def create_gui(self):
        #Setup
        #---------------------------------------
        self.gui_form = gui.Form()
        self.gui_app = gui.App(config.gui_theme)
        self.ev_manager.gui_app = self.gui_app
        c = gui.Container(align=-1,valign=-1)        
        #Timer Progress bar
        #---------------------------------------
        self.timer_bar = None
        self.time_increase = None
        self.minutes_left = None
        self.seconds_left = None
        self.timer_text = None
        if self.game_mode == config.TIME_CHALLENGE:
            self.time_increase = config.time_challenge_start_time
            self.timer_bar = gui.ProgressBar(config.time_challenge_start_time,0,config.max_time_bank,width=306)
            c.add(self.timer_bar, 172, 57)
        #Connections Progress bar
        #---------------------------------------
        self.connections_bar = None
        self.connections_bar = gui.ProgressBar(0,0,config.longest_trip_needed,width=306)
        c.add(self.connections_bar, 172, 83)            
        #Quit Button
        #---------------------------------------
        b = QuitButton(ev_manager=self.ev_manager)
        c.add(b, 950, 20)
        #Generate Board Button
        #---------------------------------------
        b = GenerateBoardButton(ev_manager=self.ev_manager, room=self)
        c.add(b, 500, 20)        
        #Board Size?
        #---------------------------------------
        bs = SetBoardSizeContainer(config.BOARD_LARGE, ev_manager=self.ev_manager, board=self.board)
        c.add(bs, 640, 20)         
        #Fill Board?
        #---------------------------------------  
        t = FillBoardCheckbox(config.fill_board, ev_manager=self.ev_manager)
        c.add(t, 740, 20)
        #Darkness?
        #---------------------------------------  
        t = UseDarknessCheckbox(config.use_darkness, ev_manager=self.ev_manager) 
        c.add(t, 840, 20)        
        #Initialize
        #---------------------------------------    
        self.gui_app.init(c)
    def advance_level(self):
        self.level += 1
        print 'Advancing to next level'        
        print 'New level: ' + str(self.level)
        if self.timer_bar:      
            print 'Time increase: ' + str(self.time_increase)
            self.timer_bar.value += self.time_increase
            self.time_increase = max(config.min_advance_time, int(self.time_increase * 0.9))            
        self.board = self.new_board
        self.new_board = None
        self.zoom_trip_complete = None
        self.game_clock.unpause()
    def notify(self, event):
        #Tick event
        if isinstance(event, TickEvent): 
            pygame.display.set_caption('FPS: ' + str(int(event.fps)))         
            self.render(self.screen) 
            pygame.display.update()
            #Wait to deal new board when advancing levels
            if self.zoom_trip_complete and self.zoom_trip_complete.finished:
                self.zoom_trip_complete = None
                self.ev_manager.post(UnfreezeCards())                    
                self.new_board = self.deck.deal_new_board(self.board)
                self.ev_manager.post(NewBoardComplete(self.new_board))
            #New high score?
            if self.zoom_game_over and self.zoom_game_over.finished and not self.highs_dialog:
                if config.current_highs.check(self.level) != None:
                    self.zoom_game_over.visible = False
                    data = 'time:' + str(self.game_clock.time) + ',swaps:' + str(self.swap_counter)
                    self.highs_dialog = HighScoreDialog(score=self.level, data=data, ev_manager=self.ev_manager)
                    self.highs_dialog.open()
                elif not self.fade_out:
                    self.fade_out = FadeOut(self.ev_manager, config.TITLE_SCREEN)
        #Second event
        elif isinstance(event, SecondEvent):
            if self.timer_bar:
                if not self.game_clock.paused:      
                    self.timer_bar.value -= 1 
                if self.timer_bar.value <= 0 and not self.game_over:
                    self.ev_manager.post(GameOver())
                self.minutes_left = self.timer_bar.value / 60
                self.seconds_left = self.timer_bar.value % 60
                if self.seconds_left < 10:
                    leading_zero = '0'
                else:
                    leading_zero = ''
                self.timer_text = ''.join(['Time Left: ', str(self.minutes_left), ':', leading_zero, str(self.seconds_left)])
        #Game over
        elif isinstance(event, GameOver):
            self.game_over = True
            self.zoom_game_over = ZoomImage(self.ev_manager, self.game_over_text)
        #Trip complete event
        elif isinstance(event, TripComplete):         
            print 'You did it!'
            self.game_clock.pause()
            self.zoom_trip_complete = ZoomImage(self.ev_manager, self.trip_complete_text)
            self.new_board_timer = Timer(self.ev_manager, 2)
            self.ev_manager.post(FreezeCards())
            print 'Room posted newboardcomplete'  
        #Board Refresh Complete
        elif isinstance(event, BoardRefreshComplete):
            if event.board == self.board:
                print 'Longest trip needed: ' + str(config.longest_trip_needed)
                print 'Your longest trip: ' + str(self.board.longest_trip)
                if self.board.longest_trip >= config.longest_trip_needed:
                    self.ev_manager.post(TripComplete())            
            elif event.board == self.new_board:
                self.advance_level()
            self.connections_bar.value = self.board.longest_trip
            self.connection_text = ' '.join(['Connections:', str(self.board.longest_trip), '/', str(config.longest_trip_needed)])
        #CardSwapComplete
        elif isinstance(event, CardSwapComplete):
            self.swap_counter += 1     
        elif isinstance(event, ConfigChangeBoardSize): 
            config.board_size = event.new_size
        elif isinstance(event, ConfigChangeCardSize): 
            config.card_size = event.new_size       
        elif isinstance(event, ConfigChangeFillBoard): 
            config.fill_board = event.new_value          
        elif isinstance(event, ConfigChangeDarkness): 
            config.use_darkness = event.new_value                        
    def render(self, surface):
        #Background
        surface.blit(self.background, (0, 0))
        #Map
        self.map.render(surface)  
        #Board      
        self.board.render(surface)
        #Logo
        surface.blit(self.logo, (10,10))
        #Text
        connection_text = self.font.render(self.connection_text, True, BLACK)
        surface.blit(connection_text, (25, 84))
        if self.timer_text:
            timer_text = self.font.render(self.timer_text, True, BLACK)
            surface.blit(timer_text, (25, 64))        
        #GUI
        self.gui_app.paint(surface)   
        if self.zoom_trip_complete:
            self.zoom_trip_complete.render(surface)     
        if self.zoom_game_over:
            self.zoom_game_over.render(surface)
        if self.fade_out:
            self.fade_out.render(surface)
class HighScoresRoom(Room):
    def __init__(self, screen, ev_manager):
        Room.__init__(self, screen, ev_manager)
        self.background = pygame.image.load('assets/images/interface/background.jpg').convert()      
        #Initialize
        #---------------------------------------
        self.gui_app = gui.App(config.gui_theme)
        self.ev_manager.gui_app = self.gui_app
        c = gui.Container(align=0,valign=0)        
        #High Scores Table
        #---------------------------------------
        hst = HighScoresTable()
        c.add(hst, 0, 0)    
        self.gui_app.init(c)
    def render(self, surface):
        surface.blit(self.background, (0, 0))
        #GUI
        self.gui_app.paint(surface)      
game_components.py     
#Remote imports
import pygame
from pygame.locals import *
import random
import operator
from copy import copy
from math import sqrt, floor
#Local imports
import config
from events import *
from matrix import Matrix
from textrect import render_textrect, TextRectException
from hyphen import hyphenator
from textwrap2 import TextWrapper
##############################
#CONSTANTS
##############################
SCREEN_MARGIN = (10, 10)
#Colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
YELLOW = (255, 200, 0)
#Directions
LEFT = -1
RIGHT = 1
UP = 2
DOWN = -2 
#Cards
CARD_MARGIN = (10, 10)
CARD_PADDING = (2, 2)
#Card types
BLANK = 0
COUNTRY = 1
TRANSPORT = 2
#Transport types
PLANE = 0
TRAIN = 1
CAR = 2
SHIP = 3
class Timer(object):
    def __init__(self, ev_manager, time_left):
        self.ev_manager = ev_manager
        self.ev_manager.register_listener(self)
        self.time_left = time_left
        self.paused = False
    def __repr__(self):
        return str(self.time_left)
    def pause(self):
        self.paused = True
    def unpause(self):
        self.paused = False
    def notify(self, event):
        #Pause Event
        if isinstance(event, Pause):   
            self.pause() 
        #Unpause Event
        elif isinstance(event, Unpause):   
            self.unpause()                
        #Second Event
        elif isinstance(event, SecondEvent):   
            if not self.paused: 
                self.time_left -= 1   
class Chrono(object):
    def __init__(self, ev_manager, start_time=0):
        self.ev_manager = ev_manager
        self.ev_manager.register_listener(self)
        self.time = start_time
        self.paused = False
    def __repr__(self):
        return str(self.time_left)
    def pause(self):
        self.paused = True
    def unpause(self):
        self.paused = False
    def notify(self, event):
        #Pause Event
        if isinstance(event, Pause):   
            self.pause() 
        #Unpause Event
        elif isinstance(event, Unpause):   
            self.unpause()                
        #Second Event
        elif isinstance(event, SecondEvent):   
            if not self.paused: 
                self.time += 1                           
class Map(object):
    def __init__(self, continent):
        self.map_image = pygame.image.load(continent.map).convert_alpha()
        self.map_text = pygame.image.load(continent.map_text).convert_alpha()
        self.pos = (0, 0)
        self.set_color()
        self.map_image = pygame.transform.smoothscale(self.map_image, config.map_size)
        self.size = self.map_image.get_size()        
    def set_pos(self, pos):
        self.pos = pos    
    def set_color(self):
        image_pixel_array = pygame.PixelArray(self.map_image)
        image_pixel_array.replace(config.GRAY1, config.COLOR1)
        image_pixel_array.replace(config.GRAY2, config.COLOR2)
        image_pixel_array.replace(config.GRAY3, config.COLOR3)
        image_pixel_array.replace(config.GRAY4, config.COLOR4)
        image_pixel_array.replace(config.GRAY5, config.COLOR5)