Back to Code List
python

Tubby Tap

I initially made this game for a python class final project. It later was worked on MUCH more and turned into an android app. You can download the original desktop game here: all_files.zip.

You can also view the completed app here: Play Store and Amazon App Store.

   main.py

import os, sys
import pygame
from pygame.locals import *
from random import randint
from vec2d import vec2d
import shelve

import globals
from Runner import Runner
from Parallax import Parallax
from Hammer import Hammer
from Button import Button
from NumberGen import NumberGen

globals.init()
pygame.init()

def write_score(score):
    d = shelve.open(globals.SCORE_FILE)
    d["high"] = score
    d.close()
    
def read_score():
    d = shelve.open(globals.SCORE_FILE)
    score = 0
    if d.has_key("high"):
        score = d["high"]
    d.close()
    return score
    

def load_sound(name, dir):
    class NoneSound:
        def play(self): pass
    if not pygame.mixer:
        return NoneSound()
    fullname = os.path.join(dir, name)
    try:
        sound = pygame.mixer.Sound(fullname)
    except pygame.error, message:
        print 'Cannot load sound:', wav
        raise SystemExit, message
    return sound

			
def load_sliced_sprites(size, filename):
    '''
    Specs :
    	Master can be any height.
    	Sprites frames width must be the same width
    	Master width must be len(frames)*frame.width
    '''
    w = size[0]
    h = size[1]
    images = []
    master_image = pygame.image.load(os.path.join('', filename)).convert_alpha()

    master_width, master_height = master_image.get_size()
    for i in xrange(int(master_width/w)):
    	images.append(master_image.subsurface((i*w,0,w,h)))
    return images


def run():
    ''' define various objects '''
    # split sprites into iterable lists (easier for animation)
    back_sprites = load_sliced_sprites((180,264), 'img/back_small.png')
    front_sprites = load_sliced_sprites((189,264), 'img/front_small.png')
    runner_sprites = load_sliced_sprites(globals.BASE_RUNNER_SIZE, 'img/mallow_man.png')
    hammer_sprites = load_sliced_sprites(globals.BASE_HAMMER_SIZE, 'img/hammer.png')
    number_sprites = load_sliced_sprites(globals.BASE_NUMBER_SIZE, 'img/numbers.png')
    die_number_sprites = load_sliced_sprites(globals.BASE_NUMBER_SIZE, 'img/numbers.png')

    # scale our split sprites
    for p in range(0,len(back_sprites)):
        back_sprites[p] = pygame.transform.scale(back_sprites[p],(int(180*globals.SCALE_RATE), int(264*globals.SCALE_RATE)))
    for p in range(0,len(front_sprites)):
        front_sprites[p] = pygame.transform.scale(front_sprites[p],(int(189*globals.SCALE_RATE), int(264*globals.SCALE_RATE)))
    for p in range(0,len(runner_sprites)):
        runner_sprites[p] = pygame.transform.scale(runner_sprites[p],globals.RUNNER_SIZE)
    for p in range(0,len(hammer_sprites)):
        hammer_sprites[p] = pygame.transform.scale(hammer_sprites[p],globals.HAMMER_SIZE)
    for p in range(0,len(number_sprites)):
        number_sprites[p] = pygame.transform.scale(number_sprites[p],globals.NUMBER_SIZE)
    for p in range(0,len(die_number_sprites)):
        die_number_sprites[p] = pygame.transform.scale(die_number_sprites[p],globals.DIE_NUMBER_SIZE)
    
    # init objects
    walk_sound = load_sound('walk.wav','audio')
    run_sound = load_sound('run.wav','audio')
    song_sound = load_sound('song.wav','audio')
    die_sound = load_sound('die.wav','audio')
    win_sound = load_sound('win.wav','audio')
    
    walk_channel = pygame.mixer.Channel(0)
    run_channel = pygame.mixer.Channel(1)
    song_channel = pygame.mixer.Channel(3)
    die_channel = pygame.mixer.Channel(4)
    win_channel = pygame.mixer.Channel(5)
    
    back = Parallax(images = back_sprites,
                    fps = globals.FPS,
                    speed = globals.BACK_SPEED, flag=1)

    front = Parallax(images = front_sprites,
                     fps = globals.FPS,
                     speed = globals.FRONT_SPEED)
    
    runner = Runner(images = runner_sprites,
                    fps = globals.FPS,
                    speed = globals.RUNNER_SPEED,
                    location = (globals.WINDOW_SIZE[0]/2-globals.RUNNER_SIZE[0]/2,
                                globals.RUNNER_POS[1]-globals.RUNNER_SIZE[1]),
                    destination = vec2d((globals.RUNNER_POS[0],
                                         globals.RUNNER_POS[1]-globals.RUNNER_SIZE[1])))
                                         
    hammers = [Hammer(images = hammer_sprites,
                      fps = globals.FPS,
                      scale = globals.SCALE_RATE,
                      start_x = globals.WINDOW_SIZE[0],
                      x_speed = front.speed)
               for h in xrange(3)]
    
    startButton = Button("Start!",
                         globals.WINDOW_SIZE[0]/2-globals.START_BTN_SIZE[0]/2,
                         globals.RUNNER_POS[1]-globals.START_BTN_SIZE[1]-(40*globals.SCALE_RATE),
                         globals.START_BTN_SIZE,
                         'img/start_btn.png')
    startButton.decorate((255, 229, 145), (255, 214, 79), (135, 255, 79), (23, 21, 14))
    
    titleText = pygame.image.load(os.path.join('', 'img/title.png')).convert_alpha()
    titleText = pygame.transform.scale(titleText,globals.TITLE_SIZE)
    
    die_score = NumberGen(images = die_number_sprites,
                                   label = pygame.transform.scale(pygame.image.load(os.path.join('', 'img/score.png')).convert_alpha(),globals.DIE_SCORE_SIZE),
                                   y = globals.DIE_SCORE_POS[1],
                                   spacing = globals.NUMBER_SPACING)
                                   
    die_high = NumberGen(images = die_number_sprites,
                                    label = pygame.transform.scale(pygame.image.load(os.path.join('', 'img/high.png')).convert_alpha(),globals.DIE_HIGH_SIZE),
                                    y = globals.DIE_HIGH_POS[1],
                                    spacing = globals.NUMBER_SPACING)
                                    
    loop_score = NumberGen(images = number_sprites,
                                      label = pygame.transform.scale(pygame.image.load(os.path.join('', 'img/score.png')).convert_alpha(),globals.SCORE_SIZE),
                                      x = globals.SCORE_POS[0],
                                      y = globals.SCORE_POS[1],
                                      spacing = globals.NUMBER_SPACING)
    ''' end objects '''
    
    # game loop variables init
    frames_passed = 0
    pre_die_pause = globals.FPS
    sprint = False
    is_running = False
    back_incr = globals.BACK_SPEED
    front_incr = globals.FRONT_SPEED
    max_i = 4
    i = 1
    di = 4
    hammer_started = False
    passed_hammers = 0 # our score
    screen_state = "front" #options are: front, pre-loop, loop, pre-died, died
    run_speed = globals.FPS*di
    diedFlag = False
    high_flag = False
    scoreWrittenFlag = False
    
    
    def play_run_sound():
        walk_channel.stop()
        run_channel.stop()
        #if not run_channel.get_busy():
        run_channel.play(run_sound)
        run_channel.set_endevent(89)
            
    def play_walk_sound():
        run_channel.stop()
        if not walk_channel.get_busy():
            walk_channel.play(walk_sound, -1)
            
    def play_die_sound():
        walk_channel.stop()
        run_channel.stop()
        song_channel.stop()
        if not die_channel.get_busy():
            die_channel.play(die_sound)
            die_channel.set_volume(0.3)
            
    def play_song():
        if not song_channel.get_busy():
            song_channel.play(song_sound, -1)
            song_channel.set_volume(0.2)
            
    def play_win_sound():
        if not win_channel.get_busy():
            win_channel.play(win_sound)
            win_channel.set_volume(0.3)
            
    play_song()
    play_walk_sound()

    # start game loop
    while True:            
        ''' listen for events '''
        for event in pygame.event.get():
            #print event
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
            if event.type == MOUSEBUTTONDOWN and screen_state != "pre-died" and screen_state != "died":
                play_run_sound()
                sprint = True
                is_running = True
                frames_passed = 0
            if event.type == KEYDOWN:
                if event.key == K_SPACE:
                    if screen_state == "front":
                        screen_state = "pre-loop"
                    elif screen_state == "loop":
                        play_run_sound()
                        sprint = True
                        is_running = True
                        frames_passed = 0
                    elif screen_state == "died":
                        diedFlag = True
            if event.type == 89:
                play_walk_sound()
        ''' end events '''

        # set game time
        time_passed = globals.CLOCK.tick(globals.FPS) #fps
        time_passed_seconds = time_passed / 1000.0
        cur = pygame.time.get_ticks()
        
        globals.SCREEN.blit(globals.BACKGROUND, (0, 0))
        
        #if sprint and screen_state != "died" and screen_state != "pre-died":
        #    play_run_sound()

        '''this section creates smooth transitions from run to walk (16 total frames on a click)'''
        #on mousedown, it accelerates quickly (3 frame transition)
        #gives a sprint allowance (4 frames)
        #then gives a little longer slow down (5 frame transition)
        if (True):
            if sprint and i < max_i:
                i+=1
                di-=1
            elif sprint and frames_passed < 4:
                frames_passed += 1
            elif frames_passed <= 4 and frames_passed > 1:
                frames_passed -= 1
                i-=1
                di+=1
                sprint = False
                if frames_passed == 1:
                    frames_passed = 1
                    is_running = False
            
            # set current frame running speed
            run_speed = globals.FPS*di
            back.speed = back_incr * i
            front.speed = front_incr * i
            for h in hammers:
                h.x_speed = front.speed
        ''' end sprint section '''

        ''' died screen '''
        if screen_state == "died":
            walk_channel.stop()
            # reset hammers and show dead runner
            for h in hammers:
                h.reset()
            
            # draw background and dead runner
            runner.destination = (globals.WINDOW_SIZE[0]/2-globals.RUNNER_SIZE[0]/2,
                                         globals.RUNNER_POS[1]-globals.RUNNER_SIZE[1])
            if (runner.destination != runner.location):
                back.process(time_passed_seconds, globals.SCREEN)
                front.process(time_passed_seconds, globals.SCREEN)
                runner.process(time_passed_seconds, globals.SCREEN)
            else:
                back.render(globals.SCREEN)
                front.render(globals.SCREEN)
                runner.render(globals.SCREEN)
            
            # do high score logic and blit high and current scores
            if not scoreWrittenFlag:
                high = read_score()
                if high > 999999:
                    write_score(999999)
                    
                if (passed_hammers > 999999):
                    high = 999999
                    write_score(999999)
                    high_flag = True
                elif (high < passed_hammers):
                    write_score(passed_hammers)
                    high = passed_hammers
                    high_flag = True
                scoreWrittenFlag = True
            
            text_str = "Score: "+str(passed_hammers)
            if high_flag:
                play_win_sound()
                text_str = "NEW HIGH SCORE!"
                if high == 999999:
                    text_str = "Holy Crap! You've Won!"
                font = pygame.font.Font(None, int(globals.SCORE_TEXT_SIZE+10))
                text = font.render(text_str, 1, (245, 240, 233))
                globals.SCREEN.blit(text, (int(globals.WINDOW_SIZE[0]/2 - text.get_width()/2),globals.TITLE_POS[1]))
            else:
                die_score.draw(passed_hammers, globals.SCREEN)
            
            '''text2_str = "High Score: "+str(high)
            if high_flag:
                text2_str = str(high)
            font2 = pygame.font.Font(None, int(globals.SCORE_TEXT_SIZE+10))
            text2 = font.render(text2_str, 1, (245, 240, 233))
            globals.SCREEN.blit(text2, (int(globals.WINDOW_SIZE[0]/2 - text2.get_width()/2),globals.TITLE_POS[1]+text.get_height()+3*globals.SCALE_RATE))'''
            
            die_high.draw(high, globals.SCREEN)
            
            # add start button
            startButton.update(pygame.mouse.get_pos(), pygame.mouse.get_pressed())
            startButton.draw(globals.SCREEN)
            # reset things when the button is clicked so we can start over
            if startButton.getState() == "up" or diedFlag:
                play_walk_sound()
                play_song()
                screen_state = "pre-loop"
                # re-init base vars
                frames_passed = 0
                sprint = False
                is_running = False
                i = 1
                di = 4
                hammer_started = False
                passed_hammers = 0 # our score
                run_speed = globals.FPS*di
                diedFlag = False
                high_flag = False
                scoreWrittenFlag = False
                
                runner.reset()
        ''' end died screen '''

        ''' transition to died screen '''
        if screen_state == "pre-died":
            if not runner.on_ground() and not runner.die_hammer.on_ground():
                if pre_die_pause == 30:
                    play_die_sound()
                    pre_die_pause=globals.FPS # reset for next run
                else:
                    pre_die_pause-=1
                    
            # render backgrounds
            back.render(globals.SCREEN)
            front.render(globals.SCREEN)
            
            # transition to died screen if animations complete
            if runner.on_ground() and runner.die_hammer.on_ground():
                if pre_die_pause == 0:
                    screen_state = "died"
                    pre_die_pause=globals.FPS # reset for next run
                else:
                    pre_die_pause-=1
                    
            # render our hammers
            for h in hammers:
                if not h.on_ground() and runner.die_hammer == h:
                    h.process(time_passed_seconds, globals.SCREEN, True)
                else:
                    h.render(globals.SCREEN)
            
            # if runner not dead yet, kill him
            if not runner.on_ground():
                runner.die(cur, 5, globals.SCREEN) # 5 is the fps override
            else:
                runner.render(globals.SCREEN)
        ''' end died transition '''
        
        ''' main game loop '''
        if screen_state == "loop":
            # add animated runner and background
            back.process(time_passed_seconds, globals.SCREEN)
            front.process(time_passed_seconds, globals.SCREEN)
            if is_running:
                runner.run(cur, run_speed, globals.SCREEN)
            else:
                runner.walk(cur, run_speed, globals.SCREEN)
            
            # manage hammer loop
            right_hammer_init = globals.WINDOW_SIZE[0] - globals.HAMMER_SEPARATION
            
            if hammer_started is False:
                hammers[0].process(time_passed_seconds, globals.SCREEN)
                hammer_started = True
                    
            if hammers[2].get_cur_x() <= right_hammer_init or hammers[0].is_started() is True:
                hammers[0].process(time_passed_seconds, globals.SCREEN)
            if hammers[0].get_cur_x() <= right_hammer_init or hammers[1].is_started() is True:
                hammers[1].process(time_passed_seconds, globals.SCREEN)
            if hammers[1].get_cur_x() <= right_hammer_init or hammers[2].is_started() is True:
                hammers[2].process(time_passed_seconds, globals.SCREEN)

            # check for a hit on any hammer
            for h in hammers:
                if runner.has_collided(h):
                    screen_state = "pre-died"
                    runner.die_hammer = h
            
            # Display counter
            hammer_count_flag = globals.RUNNER_POS[0] - globals.HAMMER_SIZE[0]
            for h in hammers:  
                if h.get_cur_x() <= hammer_count_flag and h.counted is False:
                    passed_hammers += 1
                    h.counted = True
            
            '''font = pygame.font.Font(None, int(globals.SCORE_TEXT_SIZE))
            text = font.render("Score: "+str(passed_hammers), 1, (245, 240, 233))
            globals.SCREEN.blit(text, (globals.SCORE_POS))'''
            
            loop_score.draw(passed_hammers, globals.SCREEN)
        ''' end main game loop '''
    
        ''' transition from start screen to main loop '''    
        if screen_state == "pre-loop":
            # add moving runner and background
            back.process(time_passed_seconds, globals.SCREEN)
            front.process(time_passed_seconds, globals.SCREEN)
            if is_running:
                runner.run(cur, run_speed, globals.SCREEN)
            else:
                runner.walk(cur, run_speed, globals.SCREEN)
            
            # move runner to loop position
            runner.process(time_passed_seconds, globals.SCREEN)
            if (runner.location == runner.destination):
                screen_state = "loop"
        ''' end start transition '''
            
        ''' start screen '''
        if screen_state == "front":
            # add moving runner and background
            back.process(time_passed_seconds, globals.SCREEN)
            front.process(time_passed_seconds, globals.SCREEN)
            if is_running:
                runner.run(cur, run_speed, globals.SCREEN)
            else:
                runner.walk(cur, run_speed, globals.SCREEN)
            
            #game title
            globals.SCREEN.blit(titleText,globals.TITLE_POS)
            
            # add start button
            #print pygame.mouse.get_pos(), pygame.mouse.get_pressed()
            startButton.update(pygame.mouse.get_pos(), pygame.mouse.get_pressed())
            startButton.draw(globals.SCREEN)
            if startButton.getState() == "up":
                screen_state = "pre-loop"
                
            font = pygame.font.Font(None, int(globals.SCORE_TEXT_SIZE))
            text = font.render("*Tap* to sprint", 1, (245, 240, 233))
            globals.SCREEN.blit(text, (int(globals.WINDOW_SIZE[0]/2 - text.get_width()/2),globals.HELP_POS[1]))
        ''' end start screen '''
        
        pygame.display.update()


if __name__ == "__main__":
    run()