Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/game.py
diff options
context:
space:
mode:
authorGonzalo Odiard <godiard@gmail.com>2011-10-29 01:16:54 (GMT)
committer Gonzalo Odiard <godiard@gmail.com>2011-10-29 01:16:54 (GMT)
commit3fdf076f33fada2e7c0d68c554187334aa10731d (patch)
tree3691426792becb30839be6e1db75e692298595de /game.py
parent42367fa8e47d51dd50e50b32c33bf396df87babc (diff)
Added files with pygame part to the main activity
Diffstat (limited to 'game.py')
-rwxr-xr-xgame.py495
1 files changed, 495 insertions, 0 deletions
diff --git a/game.py b/game.py
new file mode 100755
index 0000000..daaec08
--- /dev/null
+++ b/game.py
@@ -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()