diff options
author | Gonzalo Odiard <godiard@gmail.com> | 2011-10-29 01:16:54 (GMT) |
---|---|---|
committer | Gonzalo Odiard <godiard@gmail.com> | 2011-10-29 01:16:54 (GMT) |
commit | 3fdf076f33fada2e7c0d68c554187334aa10731d (patch) | |
tree | 3691426792becb30839be6e1db75e692298595de /game.py | |
parent | 42367fa8e47d51dd50e50b32c33bf396df87babc (diff) |
Added files with pygame part to the main activity
Diffstat (limited to 'game.py')
-rwxr-xr-x | game.py | 495 |
1 files changed, 495 insertions, 0 deletions
@@ -0,0 +1,495 @@ +#!/usr/bin/python +import os +import csv +import math +import simplejson as json + +import pygame +import gtk + +DATA_DIR = 'images' +STROKE_COLOR = 0, 0, 0 # black +FILL_COLOR = 255, 255, 255 # white +KEY_COLOR = 0, 255, 255 # cyan +BACKGROUND_COLOR = 10, 10, 10 # dark gray +FRAMES_PER_SECOND = 12 +SCREEN_SIZE = 320, 240 +AT_2X_SCALE = False +DEBUG = False + + +class ImageManager(object): + def __init__(self): + self.images = {} + + def _load_image(self, name, set_keycolor=False): + image_filename = name + '.png' + image_path = os.path.join(DATA_DIR, image_filename) + image = pygame.image.load(image_path) + image = image.convert() + if set_keycolor: + image.set_colorkey(KEY_COLOR) + if AT_2X_SCALE: + image = pygame.transform.scale2x(image) + return image + + def get_image(self, name, colors=None, set_keycolor=False): + """Get an image by name, load it if necessary. + + If the image wasn't get previously, load it from the + filesystem. The next time it is asked, the load will not be + made. + + If colors are given, return a copy of the image, colorized + with stroke and fill colors. + + If set_keycolor is True, the KEY_COLOR color will become + transparent in the image. + + """ + image = self.images.get(name) + if image is None: + image = self._load_image(name, set_keycolor) + self.images[name] = image + + if colors is None: + return image + else: + new_image = image.copy() + stroke_color, fill_color = colors + pixel_array = pygame.PixelArray(new_image) + pixel_array.replace(FILL_COLOR, fill_color, 0.1) + pixel_array.replace(STROKE_COLOR, stroke_color, 0.1) + return new_image + + +IMAGE_MANAGER = ImageManager() + + +def _load_animation_data(name): + """Return the animation metadata parsing a text file.""" + data_filename = name + '.json' + data_path = os.path.join(DATA_DIR, data_filename) + f = open(data_path, 'rb') + reader = json.load(f) + displacement = reader['displacement'] + frames_data = [] + for frame in list(reader['frames']): + area = frame[:4] + delta = list(frame[4:]) + frames_data.append({'area': area, 'delta': delta}) + return frames_data, displacement + + +class CharacterAnimation(object): + def __init__(self, name, colors=None, set_keycolor=True): + self._frames_image = IMAGE_MANAGER.get_image(name, + colors, set_keycolor) + self._frames_image_mirror = pygame.transform.flip(self._frames_image, + True, False) + self._frames_data, self._displacement = _load_animation_data(name) + if AT_2X_SCALE: + self._resize_frames_data() + self._convert_frames_data() + self._mirror_frames_data() + self._frames_len = len(self._frames_data) + self._cur_frame = None + self._is_playing = None + self._direction = None + self._mirror = None + + def _resize_frames_data(self): + """ Multiply the area data by two. + + This needs to be done if displaying images at 2x. + + """ + for data in self._frames_data: + area = data['area'] + resized_area = tuple((elem * 2 for elem in area)) + data['area'] = resized_area + + def _convert_frames_data(self): + """Convert the area data to pygame rects.""" + for data in self._frames_data: + area = data['area'] + converted_area = pygame.Rect(*area) + data['area'] = converted_area + + def _mirror_frames_data(self): + self._frames_data_mirror = [] + for data in self._frames_data: + mirror_data = {} + mirror_data['area'] = data['area'].copy() + mirror_data['area'].left = self._frames_image.get_width() \ + - data['area'].left - data['area'].width + mirror_data['delta'] = list(data['delta']) + mirror_data['delta'][0] = -data['delta'][0] - data['area'].width + self._frames_data_mirror.append(mirror_data) + + def play(self, origin=(0, 0), direction='forward', mirror=False): + self._origin = origin + if direction == 'forward': + self._cur_frame = 0 + elif direction == 'backward': + self._cur_frame = self._frames_len - 1 + else: + raise NotImplementedError + self._direction = direction + self._mirror = mirror + self._is_playing = True + + def stop(self): + self._is_playing = False + + def get_dist_to_rect(self): + if self._mirror: + data = self._frames_data_mirror[self._cur_frame] + else: + data = self._frames_data[self._cur_frame] + dx = data['delta'][0] + (data['area'].width / 2.0) + return dx, 0 + + def _at_last_frame(self): + return self._cur_frame == self._frames_len - 1 + + def _at_first_frame(self): + return self._cur_frame == 0 + + def _next_frame(self): + if ((self._direction == 'forward' and self._at_last_frame()) or + (self._direction == 'backward' and self._at_first_frame())): + self._is_playing = False + return 'finished' + if self._direction == 'forward': + self._cur_frame += 1 + elif self._direction == 'backward': + self._cur_frame -= 1 + + def update(self): + if self._is_playing: + return self._next_frame() + + def draw(self, surface): + if not self._mirror: + cur_frame_data = self._frames_data[self._cur_frame] + frames_image = self._frames_image + else: + cur_frame_data = self._frames_data_mirror[self._cur_frame] + frames_image = self._frames_image_mirror + area = cur_frame_data['area'] + delta = cur_frame_data['delta'] + dx = self._origin[0] + delta[0] + dy = self._origin[1] + delta[1] + return surface.blit(frames_image, (dx, dy), area) + + def get_displacement(self): + """Return the absolute displacement. + + Takes into account the direction and if the animation is + mirrored. + + """ + result = None + if self._direction == 'forward': + result = self._displacement + elif self._direction == 'backward': + result = list((elem * -1 for elem in self._displacement)) + + if self._mirror: + result = -1 * result[0], result[1] + + return result + + +class Stage(object): + """A stage contains a wall and a floor.""" + def __init__(self, horizon): + """Stage constructor. + + Horizon is a number between 0 and 1 that is used to draw the + wall and the floor above and below a line. + + """ + self._wall = IMAGE_MANAGER.get_image('wall_tile') + self._floor = IMAGE_MANAGER.get_image('floor_tile') + self._horizon = horizon + + def draw(self, surface): + """Draw the wall and the floor. + + The wall and the floor are drawn using tiles. + + """ + surface_width = surface.get_width() + surface_height = surface.get_height() + tile_width = self._wall.get_width() + tile_height = self._wall.get_height() + wall_height = surface_height * (1.0 - self._horizon) + floor_height = surface_height * self._horizon + + def get_tiles_to_fill(distance, tile_length): + """Return the number of tiles needed to fill the distance""" + return int(math.ceil(float(distance) / tile_length)) + + tiles_per_width = get_tiles_to_fill(surface_width, tile_width) + wall_tiles_per_height = get_tiles_to_fill(wall_height, tile_width) + floor_tiles_per_height = get_tiles_to_fill(floor_height, tile_width) + + # draw wall above horizon + for tile_x in range(tiles_per_width): + dx = tile_width * tile_x + for tile_y in range(wall_tiles_per_height): + dy = wall_height - (tile_height * (tile_y + 1)) + surface.blit(self._wall, (dx, dy)) + + # draw floor below horizon + for tile_x in range(tiles_per_width): + dx = tile_width * tile_x + for tile_y in range(floor_tiles_per_height): + dy = wall_height + (tile_height * tile_y) + surface.blit(self._floor, (dx, dy)) + + +class Character(object): + def __init__(self, pos_x, pos_y, direction=None, orientation=None): + self._pos_x = pos_x + self._pos_y = pos_y + self._animations = {} + self._cur_animation = None + self._on_loop = True + if orientation is None: + orientation = 'right' + self.orientation = orientation + if direction is None: + direction = 'forward' + self.direction = direction + + def add_action(self, animation_name, animation): + self._animations[animation_name] = animation + + def act(self, animation, clean=True): + """ + Play the given animation. + + The animation parameter can be an animation name, previously + passed to the add_animation method, or the animation itself. + + """ + if self._cur_animation is not None and clean: + + self._cur_animation.stop() + dx, dy = self._cur_animation.get_dist_to_rect() + self._pos_x += dx + self._pos_y += dy + if isinstance(animation, str): + self._cur_animation = self._animations[animation] + else: + self._cur_animation = animation + origin = (self._pos_x, self._pos_y) + mirror = self.orientation == 'left' + self._cur_animation.play(origin, self.direction, mirror) + + def draw(self, surface): + return self._cur_animation.draw(surface) + + def update(self): + result = self._cur_animation.update() + if result == 'finished': + displacement = self._cur_animation.get_displacement() + self._pos_x += displacement[0] + self._pos_y += displacement[1] + if self._on_loop: + self.act(self._cur_animation, clean=False) + + def flip_direction(self): + if self.direction == 'backward': + self.direction = 'forward' + elif self.direction == 'forward': + self.direction = 'backward' + + +class MouseCross(object): + def __init__(self): + self._cross = IMAGE_MANAGER.get_image('cross', set_keycolor=True) + self._pos = None + + def update(self, pos): + self._pos = pos + + def draw(self, surface): + x = self._pos[0] - (self._cross.get_width() / 2) + y = self._pos[1] - (self._cross.get_height() / 2) + return surface.blit(self._cross, (x, y)) + + +class Game(object): + """Ingenium Machina game""" + def __init__(self, colors=None): + + self._running = True + self._clock = pygame.time.Clock() + self._background = None + self._char = None + self._mouse_cross = None + self._click_x = None + if DEBUG: + self._pos_cross = None + self._rect_cross = None + self._colors = colors + if self._colors is not None: + self._convert_colors() + + def _convert_colors(self): + """Convert the given colors to pygame colors.""" + self._colors = list((pygame.Color(color) for color in self._colors)) + + def setup(self): + """Setup the game elements.""" + screen = pygame.display.get_surface() + + # graphics that don't move get blitted in this surface + self._background = screen.copy() + + horizon = 1.0 / 5 + self._stage = Stage(horizon) + + char_pos_x = screen.get_width() / 2 + char_pos_y = screen.get_height() * (1 - horizon) + self._char = Character(char_pos_x, char_pos_y) + stand_ani = CharacterAnimation('stand', self._colors) + self._char.add_action('stand', stand_ani) + walk_ani = CharacterAnimation('walk', self._colors) + self._char.add_action('walk', walk_ani) + self._char.act('stand') + + self._mouse_cross = MouseCross() + + if DEBUG: + self._pos_cross = MouseCross() + self._rect_cross = MouseCross() + + pygame.mouse.set_visible(False) + + def run(self): + """Game loop.""" + self.setup() + + screen = pygame.display.get_surface() + + screen.fill(BACKGROUND_COLOR) + self._stage.draw(self._background) + screen.blit(self._background, (0, 0)) + + pygame.display.flip() + + # a list of the affected screen areas, for updating only those + dirty_rects = [] + + while self._running: + # try to stay at the given frames per second + self._clock.tick(FRAMES_PER_SECOND) + + # pump gtk messages + while gtk.events_pending(): + gtk.main_iteration() + + # pump pygame messages + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self._running = False + elif event.type == pygame.VIDEORESIZE: + pygame.display.set_mode(event.size, pygame.RESIZABLE) + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + self._running = False + elif event.key == pygame.K_LEFT: + self._click_x = None + self._char.orientation = 'left' + self._char.act('walk') + elif event.key == pygame.K_RIGHT: + self._click_x = None + self._char.orientation = 'right' + self._char.act('walk') + elif event.type == pygame.KEYUP: + self._click_x = None + self._char.act('stand') + elif event.type == pygame.MOUSEMOTION: + mouse_pos = pygame.mouse.get_pos() + self._mouse_cross.update(mouse_pos) + + elif event.type == pygame.MOUSEBUTTONUP: + mouse_pos = pygame.mouse.get_pos() + self._click_x = mouse_pos[0] + if mouse_pos[0] < self._char._pos_x: + self._char.orientation = 'left' + else: + self._char.orientation = 'right' + self._char.act('walk') + + # clean the background, filling the affected areas of the + # screen with the background + new_dirty_rects = [] + for rect in dirty_rects: + new_rect = screen.blit(self._background, rect, rect) + new_dirty_rects.append(new_rect) + dirty_rects = new_dirty_rects + + # move graphics + if self._click_x is not None: + stop_animation = False + if self._char.orientation == 'right': + if self._char._pos_x > self._click_x: + stop_animation = True + elif self._char.orientation == 'left': + if self._char._pos_x < self._click_x: + stop_animation = True + if stop_animation: + self._char.act('stand') + self._click_x = None + self._char.update() + + if DEBUG: + self._pos_cross.update((self._char._pos_x, self._char._pos_y)) + ani = self._char._cur_animation + if ani._mirror: + data = ani._frames_data_mirror[ani._cur_frame] + else: + data = ani._frames_data[ani._cur_frame] + rect_x = ani._origin[0] + data['delta'][0] + (data['area'].width / 2.0) + self._rect_cross.update((rect_x, self._char._pos_y)) + + # draw char + rect = self._char.draw(screen) + dirty_rects.append(rect) + + if DEBUG: + rect = self._pos_cross.draw(screen) + dirty_rects.append(rect) + rect = self._rect_cross.draw(screen) + dirty_rects.append(rect) + + # draw mouse cross + rect = self._mouse_cross.draw(screen) + dirty_rects.append(rect) + + # update the display + pygame.display.update(dirty_rects) + + +def main(): + """Setup pygame and run game. + + This function is called when the game is run directly from the + command line as: ./game.py + + """ + pygame.init() + pygame.display.set_mode((0, 0), pygame.RESIZABLE) + + colors = ['#101010', '#ffffff'] + game = Game(colors) + game.run() + +if __name__ == '__main__': + main() |