import os, sys, pygame from pygame.locals import * import objects, config "This module contains the main game logic of the Squish game." class State: """ A generic game state class that can handle events and display itself on a given surface. """ def handle(self, event): """ Default event handling only deals with quitting. """ if event.type == QUIT: sys.exit() if event.type == KEYDOWN and event.key == K_ESCAPE: sys.exit() def first_display(self, screen): """ Used to display the State for the first time. Fills the screen with the background color. """ screen.fill(config.background_color) # Remember to call flip, to make the changes visible: pygame.display.flip() def display(self, screen): """ Used to display the State after it has already been displayed once. The default behavior is to do nothing. """ pass class Level(State): """ A game level. Takes care of counting how many weights have been dropped, moving the sprites around, and other tasks relating to game logic. """ def __init__(self, number=1): self.number = number # How many weights remain to dodge in this level? self.remaining = config.weights_per_level speed = config.drop_speed # One speed_increase added for each level above 1: speed += (self.number-1) * config.speed_increase # Create the weight and banana: self.weight = objects.Weight(speed) self.banana = objects.Banana() both = self.weight, self.banana # This could contain more sprites... self.sprites = pygame.sprite.RenderUpdates(both) def update(self, game): "Updates the game state from the previous frame." # Update all sprites: self.sprites.update() # If the banana touches the weight, tell the game to switch to # a GameOver state: if self.banana.touches(self.weight): game.next_state = GameOver() # Otherwise, if the weight has landed, reset it. If all the # weights of this level have been dodged, tell the game to # switch to a LevelCleared state: elif self.weight.landed: self.weight.reset() self.remaining -= 1 if self.remaining == 0: game.next_state = LevelCleared(self.number) def display(self, screen): """ Displays the state after the first display (which simply wipes the screen). As opposed to firstDisplay, this method uses pygame.display.update with a list of rectangles that need to be updated, supplied from self.sprites.draw. """ screen.fill(config.background_color) updates = self.sprites.draw(screen) pygame.display.update(updates) class Paused(State): """ A simple, paused game state, which may be broken out of by pressing either a keyboard key or the mouse button. """ finished = 0 # Has the user ended the pause? image = None # Set this to a file name if you want an image text = '' # Set this to some informative text def handle(self, event): """ Handles events by delegating to State (which handles quitting in general) and by reacting to key presses and mouse clicks. If a key is pressed or the mouse is clicked, self.finished is set to true. """ State.handle(self, event) if event.type in [MOUSEBUTTONDOWN, KEYDOWN]: self.finished = 1 def update(self, game): """ Update the level. If a key has been pressed or the mouse has been clicked (i.e., self.finished is true), tell the game to move to the state represented by self.next_state() (should be implemented by subclasses). """ if self.finished: game.next_state = self.next_state() def first_display(self, screen): """ The first time the Paused state is displayed, draw the image (if any) and render the text. """ # First, clear the screen by filling it with the background color: screen.fill(config.background_color) # Create a Font object with the default appearance, and specified size: font = pygame.font.Font(None, config.font_size) # Get the lines of text in self.text, ignoring empty lines at # the top or bottom: lines = self.text.strip().splitlines() # Calculate the height of the text (using font.get_linesize() # to get the height of each line of text): height = len(lines) * font.get_linesize() # Calculate the placement of the text (centered on the screen): center, top = screen.get_rect().center top -= height // 2 # If there is an image to display... if self.image: # load it: image = pygame.image.load(self.image).convert() # get its rect: r = image.get_rect() # move the text down by half the image height: top += r.height // 2 # place the image 20 pixels above the text: r.midbottom = center, top - 20 # blit the image to the screen: screen.blit(image, r) antialias = 1 # Smooth the text black = 0, 0, 0 # Render it as black # Render all the lines, starting at the calculated top, and # move down font.get_linesize() pixels for each line: for line in lines: text = font.render(line.strip(), antialias, black) r = text.get_rect() r.midtop = center, top screen.blit(text, r) top += font.get_linesize() # Display all the changes: pygame.display.flip() class Info(Paused): """ A simple paused state that displays some information about the game. It is followed by a Level state (the first level). """ next_state = Level text = ''' In this game you are a banana, trying to survive a course in self-defense against fruit, where the participants will "defend" themselves against you with a 16 ton weight.''' class StartUp(Paused): """ A paused state that displays a splash image and a welcome message. It is followed by an Info state. """ next_state = Info image = config.splash_image text = ''' Welcome to Squish, the game of Fruit Self-Defense''' class LevelCleared(Paused): """ A paused state that informs the user that he or she has cleared a given level. It is followed by the next level state. """ def __init__(self, number): self.number = number self.text = '''Level {} cleared Click to start next level'''.format(self.number) def next_state(self): return Level(self.number + 1) class GameOver(Paused): """ A state that informs the user that he or she has lost the game. It is followed by the first level. """ next_state = Level text = ''' Game Over Click to Restart, Esc to Quit''' class Game: """ A game object that takes care of the main event loop, including changing between the different game states. """ def __init__(self, *args): # Get the directory where the game and the images are located: path = os.path.abspath(args[0]) dir = os.path.split(path)[0] # Move to that directory (so that the image files may be # opened later on): os.chdir(dir) # Start with no state: self.state = None # Move to StartUp in the first event loop iteration: self.next_state = StartUp() def run(self): """ This method sets things in motion. It performs some vital initialization tasks, and enters the main event loop. """ pygame.init() # This is needed to initialize all the pygame modules # Decide whether to display the game in a window or to use the # full screen: flag = 0 # Default (window) mode if config.full_screen: flag = FULLSCREEN # Full screen mode screen_size = config.screen_size screen = pygame.display.set_mode(screen_size, flag) pygame.display.set_caption('Fruit Self Defense') pygame.mouse.set_visible(False) # The main loop: while True: # (1) If nextState has been changed, move to the new state, and # display it (for the first time): if self.state != self.next_state: self.state = self.next_state self.state.first_display(screen) # (2) Delegate the event handling to the current state: for event in pygame.event.get(): self.state.handle(event) # (3) Update the current state: self.state.update(self) # (4) Display the current state: self.state.display(screen) if __name__ == '__main__': game = Game(*sys.argv) game.run()