Created
June 9, 2011 03:30
-
-
Save crap0101/1015994 to your computer and use it in GitHub Desktop.
Conway's Game of Life with pygame
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
# This file is part of ImparaParole and is released under a MIT-like license | |
# Copyright (c) 2010 Marco Chieppa (aka crap0101) | |
# See the file COPYING.txt in the root directory of this package. | |
""" | |
Conway's Game of Life (modified to be used with pygame) | |
NOTE: needs the classes GenericGameObject and Grid | |
COMMANDS: | |
* any key: exit the game | |
* mouse right button: pause the evolution (in these days many people give the | |
impression to have the right button pressed :)) | |
* mouse left button: when the evolution is paused, flip the cell state. | |
""" | |
import copy | |
import os | |
import sys | |
import time | |
import random | |
import itertools as it | |
import argparse | |
import pygame | |
try: | |
from ImparaParole.baseObjects.gameObjects import GenericGameObject, Grid | |
except ImportError: | |
from gameObjects import GenericGameObject, Grid | |
COLOR_LIVE = (50,205,50,255) | |
COLOR_DEAD = (0,0,0,255) | |
CELL_COLORS = (COLOR_DEAD, COLOR_LIVE) | |
### PARSER DEF, HELP STRINGS AND OTHER STUFFS ### | |
# DISPLAY HELP STRINGS | |
H_DENSITY = 'density for the cell in the the universe to fill with.' | |
H_DELAY = 'the time (in milliseconds) between generations.' | |
H_COLUMNS = 'number of columns' | |
H_ROWS = 'number of rows' | |
H_HEIGHT = "set the window's height. Default to 0 (fullscreen if -W is 0 too)." | |
H_WIDTH = "set the window's width. Default to 0 (fullscreen if -H is 0 too)." | |
H_OPTVAL = """the initial pattern, must be a string of binary digits. 0 means | |
a dead cell, 1 means a live cell. If the pattern is too short tho fill the | |
universe, it will be repeated until every cell got a value. If the pattern | |
oversize the universe, the excess will be discarded.""" | |
H_SCR_MODE = """set the display mode. SCR_MODE must be one of recognized | |
pygame's constant for the display, eg: | |
FULLSCREEN for a fullscreen display, | |
DOUBLEBUF is recommended for HWSURFACE or OPENGL, | |
HWSURFACE for hardware accelerated (only in FULLSCREEN), | |
OPENGL for an opengl renderable display, | |
RESIZABLE to make the display window resizeable, | |
NOFRAME for a display window with no border or controls. | |
For example, to get a fullscreen display, you should pass `-s FULLSCREEN`. | |
You can include many mode names, useful if you want to combine | |
multiple types of modes, eg: -s RESIZABLE OPENGL . | |
If no size argument or any other modes are specified, fall in FULLSCREEN mode. | |
WARNING: some mode could not be available on your machine.""" | |
# MISC HELP STRING | |
H_PRINTT = "at the end of the game, print the inital table (using repr())." | |
H_PRINTG = "at the end of the game, print the number of generations (using repr())." | |
H_DESCRIPTION = "# Conway's Game of Life with Pygame #" | |
H_EPILOG = """|| any key: exit the game | |
|| mouse right button: pause the evolution | |
|| mouse left button: when the evolution is paused, flip the cell state. | |
""" | |
# OTHER CONSTANTS | |
SCR_MODE_STRINGS = ('FULLSCREEN', 'DOUBLEBUF', 'HWSURFACE', | |
'OPENGL', 'RESIZABLE', 'NOFRAME') | |
def get_parsed (args): | |
"""Parse `args' and possibly return the parsed object from | |
argparse.ArgumentParser.parse_args . | |
""" | |
parser = argparse.ArgumentParser( | |
description=H_DESCRIPTION, epilog=H_EPILOG, | |
formatter_class=argparse.ArgumentDefaultsHelpFormatter) | |
# display options | |
dislay_group = parser.add_argument_group('Display Options') | |
dislay_group.add_argument('-d', '--density', type=int, default=0, | |
dest='density', metavar='NUM', help=H_DENSITY) | |
dislay_group.add_argument('-D', '--delay', type=int, default=300, | |
dest='delay', metavar='NUM', help=H_DELAY) | |
dislay_group.add_argument('-r', '--rows', type=int, default=20, | |
dest='rows', metavar='NUM', help=H_ROWS) | |
dislay_group.add_argument('-c', '--columns', type=int, default=20, | |
dest='cols', metavar='NUM', help=H_COLUMNS) | |
dislay_group.add_argument('-i', '--initial', type=str, default=None, | |
dest='optvalues', metavar='STRING', help=H_OPTVAL) | |
dislay_group.add_argument('-H', '--height', type=int, default=0, | |
dest='h', metavar='NUM', help=H_HEIGHT) | |
dislay_group.add_argument('-W', '--width', type=int, default=0, | |
dest='w', metavar='NUM', help=H_WIDTH) | |
dislay_group.add_argument('-s', '--scr-mode', default=(), | |
dest='scr_mode', nargs='+', metavar='SCR_MODE', | |
choices=SCR_MODE_STRINGS, help=H_SCR_MODE) | |
# misc options | |
parser.add_argument('-v', '--version', action='version', | |
help="show version informations and exit", | |
version='%(prog)s 0.2') | |
parser.add_argument('-p', '--print-inittable', action='store_true', | |
dest='printt', help=H_PRINTT) | |
parser.add_argument('-g', '--print-gen', action='store_true', | |
dest='printg', help=H_PRINTG) | |
return parser.parse_args(args) | |
def check_mode(modelist): | |
"""Return the pygame's video mode for the display combining | |
the modes in `modelist' or raise an AttributeError if some | |
modes are unknown. | |
""" | |
mode = 0 | |
try: | |
for _mode in modelist: | |
mode |= getattr(pygame, _mode) | |
except AttributeError: | |
raise AttributeError("ERROR: unknown display mode %s\n" % repr(_mode)) | |
return mode | |
### GAME CLASSES ### | |
class Point(object): | |
"""Point class with some convenience methods.""" | |
def __init__(self, x, y): | |
self.x = x | |
self.y = y | |
self._start_cpos = -1 | |
self._coord = self._start_cpos | |
@property | |
def values(self): | |
"""The Point coordinates.""" | |
return self.x, self.y | |
@values.setter | |
def values(self, new_values): | |
"""Set the Point coordinates (x,y) to *newvalues.""" | |
self.x, self.y = new_values | |
def __add__(self, other_point): | |
try: | |
return Point(self.x + other_point.x, self.y + other_point.y) | |
except AttributeError: | |
return Point(self.x + other_point[0], self.y + other_point[1]) | |
__radd__ = __add__ | |
def __iadd__(self, other_point): | |
self.values = self.__add__(other_point).values | |
return self | |
def __iter__(self): | |
return self | |
def next(self): | |
try: | |
self._coord += 1 | |
return self.values[self._coord] | |
except IndexError: | |
self._coord = self._start_cpos | |
raise StopIteration | |
def __str__(self): | |
return "%s" % (self.values,) | |
def __repr__(self): | |
return "%s%s" % (self.__class__.__name__, self.values) | |
class ConwayGame(object): | |
"""A class which implements the Conway's game of life with pygame.""" | |
def __init__(self, screen, nrows, ncols, optvalues, density, delay): | |
self.screen = screen or pygame.display.set_mode((0,0), pygame.FULLSCREEN) | |
self.pyGrid = Grid(GenericGameObject) | |
self.pyGrid.build(ncols, nrows, (2,3)) | |
self.pyGrid.resize(*self.screen.get_size()) | |
self.delay = (10, delay) | |
self.nrows = nrows | |
self.ncols = ncols | |
self.old_table = [] | |
self.generations = 0 | |
self.table = [[False]*ncols for i in range(nrows)] | |
if not optvalues: | |
for i in range(self.nrows): | |
for j in range(int(min(nrows,ncols)**0.5+density)): | |
self.table[i][random.randint(0, ncols-1)] = True | |
else: | |
cycle = it.cycle(optvalues) | |
for row in range(self.nrows): | |
for col in range(self.ncols): | |
self.table[row][col] = bool(int(cycle.next())) | |
self.neighbors = [Point(i, j) for i in range(-1,2) for j in range(-1,2) if (i or j)] | |
self.inittable = copy.deepcopy(self.table) | |
def get_neighbor(self, point): | |
"""Return True if `point' is in grid.""" | |
x, y = point | |
return True if (-1 < x < self.nrows) and (-1 < y < self.ncols) else False | |
def check_alive(self, row, col): | |
"""Check if the cell in the grid at (row,col) is alive, returning | |
its number of neighbors. | |
""" | |
_neighbors = [p+(row, col) for p in self.neighbors] | |
return sum(self.table[p.x][p.y] for p in _neighbors if self.get_neighbor(p)) | |
def display(self): | |
"""display the acutal generation.""" | |
radius = min(self.pyGrid[0].rect.size)/2 | |
for cell in self.pyGrid: | |
row, col = self.pyGrid.current_coord | |
color = self.table[row][col] | |
pygame.draw.circle(self.screen, CELL_COLORS[color], cell.rect.center, radius, 0) | |
""" # quadrilateral: | |
cell.surface.fill(CELL_COLORS[color]) | |
for cell in self.pyGrid: | |
cell.draw_on(self.screen) | |
#""" | |
pygame.display.update() | |
def check_evo(self): | |
"""Return False id the universe don't evolve anymore.""" | |
return self.table != self.old_table | |
def play(self): | |
"""Compute and display another generation in the universe.""" | |
new_table = copy.deepcopy(self.table) | |
for row in range(self.nrows): | |
for col in range(self.ncols): | |
alive_val = self.check_alive(row, col) | |
if self.table[row][col]: | |
if alive_val not in (2, 3): | |
new_table[row][col] = False | |
elif alive_val == 3: | |
new_table[row][col] = True | |
self.table = copy.deepcopy(new_table) | |
self.generations += 1 | |
self.display() | |
if not self.check_evo(): | |
return False | |
self.old_table = copy.deepcopy(self.table) | |
return True | |
def start(self): | |
"""Start the game.""" | |
self.display() | |
self._playing = True | |
while True: | |
for event in pygame.event.get(): | |
if event.type in (pygame.QUIT, pygame.KEYDOWN): | |
return self.generations | |
if event.type == pygame.MOUSEBUTTONDOWN: | |
mouse_buttons = pygame.mouse.get_pressed() | |
if mouse_buttons[2]: | |
self._playing ^= True | |
elif mouse_buttons[0] and not self._playing: | |
mouse_pos = pygame.mouse.get_pos() | |
for cell in self.pyGrid: | |
if cell.rect.collidepoint(mouse_pos): | |
row, col = self.pyGrid.current_coord | |
self.table[row][col] ^= True | |
self.pyGrid.rewind() | |
self.display() | |
if self._playing: | |
if not self.play(): | |
return self.generations | |
pygame.time.wait(self.delay[self._playing]) | |
if __name__ == '__main__': | |
parser = get_parsed(sys.argv[1:]) | |
rows = parser.rows | |
cols = parser.cols | |
optvalues = parser.optvalues | |
density = parser.density | |
delay = parser.delay | |
pygame.init() | |
screen = pygame.display.set_mode((parser.w, parser.h), | |
check_mode(parser.scr_mode)) | |
g = ConwayGame(screen, rows, cols, optvalues, density, delay) | |
g.start() | |
if parser.printt: | |
print repr(g.inittable) | |
if parser.printg: | |
print repr(g.generations) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
# This file is part of ImparaParole and is released under a MIT-like license | |
# Copyright (c) 2010 Marco Chieppa (aka crap0101) | |
# See the file COPYING.txt in the root directory of this package. | |
""" | |
This module contains the basic stuffs needed to build and use | |
various type of game objects. | |
""" | |
import random | |
from collections import defaultdict, namedtuple | |
import pygame | |
try: # modified for play ConwayGameOfLife without other modules | |
from ..miscUtils.gameutils import copy_rect, rect_area | |
except (ImportError, ValueError): | |
def copy_rect (rect): | |
"""return a new rect, a copy of the argument `rect`. | |
This is a workaround to mimic the copy method, for Pygame < 1.9.0 . | |
""" | |
return pygame.Rect(rect) | |
def rect_area (rect): | |
"""Return the area of the rect argument.""" | |
return rect.width * rect.height | |
class GenericGameObject (object): | |
"""A generic game object. Provide method for draw, | |
move, resize, etc.""" | |
def __init__ (self, surface=None, cmp_value=None): | |
"""Set 'surface' as its own surface or create a new | |
zero-sized surface; also set the 'rect' attribute as | |
the surface's rect. | |
'cmp_value' is the value used to compare similar objects, | |
e.g. obj1 == obj2 . If not provided fall to id(self). | |
""" | |
self.surface = surface if surface else pygame.Surface((0,0)) | |
self._original_surface = self.surface.copy() | |
self.rect = self.surface.get_rect() | |
self.surround_rect = copy_rect(self.rect) | |
self.surround_rect_delta = (0,0) # the delta between self.rect and the | |
# surround rect. Used to update the | |
# latter when changing the object's | |
# rect size from the object's methods. | |
self.update_surround_rect() | |
self._cmp_value = cmp_value if cmp_value is not None else id(self) | |
self.actions = defaultdict(list) | |
self.velocity = [5, 7] | |
@property | |
def area (self): | |
"""The object's rect area""" | |
return self.rect.width * self.rect.height | |
@property | |
def size (self): | |
"""Return the object's rect size.""" | |
return self.rect.size | |
@size.setter | |
def size (self, xy_size): | |
"""Resize the object at the given 'xysize'.""" | |
self.resize(*xy_size) | |
@property | |
def compare_value (self): | |
"""Return the value used to compare this objects.""" | |
return self._cmp_value | |
@compare_value.setter | |
def compare_value (self, cmp_value): | |
"""Set the value used to compare this objects.""" | |
self._cmp_value = cmp_value | |
def __eq__(self, other): | |
return self._cmp_value == other | |
def __ne__ (self, other): | |
return self._cmp_value != other | |
def clamp (self, rect): | |
"""Move the object's rect to be completely inside | |
the argument 'rect'. Return the new rect. | |
""" | |
self.rect = self.rect.clamp(rect) | |
self.update_surround_rect() | |
return self.rect | |
def clamp_ip (self, rect): | |
"""Like clamp but operates in place. Return None.""" | |
self.rect.clamp_ip(rect) | |
self.update_surround_rect() | |
def draw_on (self, surface, background=None, dict_or_seq=None): | |
"""Draw the object on 'surface', possibly changing its | |
rect's attributes using 'dict_or_seq'. If 'background' | |
is not None, it must be a surface intended to be drawed | |
on 'surface' *before* the object's surface (and at the | |
same position). | |
""" | |
if dict_or_seq: | |
self.set_rect_attrs(dict_or_seq) | |
if background: | |
surface.blit(background, self.rect, self.rect) | |
return surface.blit(self.surface, self.rect) | |
def erase (self, destination, surface, clip_area=None): | |
"""Erase the object's surface from 'destination', drawing 'surface' | |
in its rect's area. 'clip_area' represents a smaller portion of the | |
destination surface to draw. | |
""" | |
return destination.blit(surface, self.rect, clip_area) | |
def fit (self, rect_or_surface): | |
"""Move and resize the object's rect to fit 'other_rect'. | |
The aspect ratio of the original Rect is preserved, so the new | |
rectangle may be smaller than the target in either width or height. | |
""" | |
try: | |
self.resize(*self.rect.fit(rect_or_surface.get_rect()).size) | |
except AttributeError: | |
self.resize(*self.rect.fit(rect_or_surface).size) | |
def is_clicked (self, point=None): | |
"""Return True if the given 'point' is inside the object's rect. | |
A point along the right or bottom edge is not considered to be | |
inside the rectangle. If 'point' is not provided use the point value | |
got from pygame.mouse.get_pos().""" | |
return self.rect.collidepoint(point or pygame.mouse.get_pos()) | |
def move (self, x, y): | |
"""Move the object's rect by the given offset. 'x' and 'y' can be | |
any integer value, positive or negative. Return the moved rect.""" | |
self.rect = self.rect.move(x, y) | |
self.update_surround_rect() | |
return self.rect | |
def move_at (self, position, anchor_at='center'): | |
"""Move the object's rect at the given position 'position'. | |
The 'anchor_at' arg (default 'center') must be a string representing | |
a valid rect attribute and its value will be the new position. | |
""" | |
setattr(self.rect, anchor_at, position) | |
self.update_surround_rect() | |
return self.rect | |
def move_ip (self, x, y): | |
"""Like 'move' but operates in place. Return None.""" | |
self.rect.move_ip(x, y) | |
self.update_surround_rect() | |
def move_bouncing (self, bounce_rect, velocity=(None, None)): | |
"""Move the object's rect bouncing inside 'bounce_rect' | |
by 'velocity' (a tuple, default to self.velocity). | |
""" | |
x, y = velocity if any(velocity) else self.velocity | |
new_velocity = [x, y] | |
if (self.rect.left + x <= bounce_rect.left or | |
self.rect.right + x >= bounce_rect.right): | |
new_velocity[0] = -x | |
if (self.rect.bottom + y >= bounce_rect.bottom or | |
self.rect.top + y <= bounce_rect.top): | |
new_velocity[1] = -y | |
self.move_ip(x, y) #*new_velocity) | |
self.clamp_ip(bounce_rect) | |
self.velocity = new_velocity | |
return self.rect | |
def move_random (self, in_rect, x=(None,None), y=(None,None)): | |
"""Move the object's rect inside 'in_rect' by a random value. | |
If the optional args 'x' and 'y' are provided they must be | |
a pair of integer - positive or negative - and are used as the | |
range inside each coordinate's move will performed, e.g. x=(min, max). | |
If a pair element is None it's value belong to the 'in_rect' dimension | |
(for max) or zero (for min). If 'min' > 'max' their values | |
are silently sorted. Raise TypeError if the 'x' and 'y' | |
tuples have more or less items required or if the 'in_rect' | |
arg is not a valid object. Return the moved rect. | |
""" | |
try: | |
min_x, max_x = sorted(x) | |
min_y, max_y = sorted(y) | |
w, h = in_rect.size | |
max_x = w if max_x is None else max_x | |
max_y = h if max_y is None else max_y | |
self.move_ip(random.randint(min_x or 0, max_x), | |
random.randint(min_y or 0, max_y)) | |
except (TypeError, ValueError), err: | |
raise TypeError("%s: 'x' and 'y' args must be tuples of two " | |
"integer items (or None, eg: (None, 100))" % err) | |
except AttributeError, err: | |
raise TypeError(str(err)) | |
self.clamp_ip(in_rect) | |
return self.rect | |
def resize (self, width, height): | |
"""Resize the object at the new (width, height) dimension. | |
The two args 'width' and 'height' must be two positive integer, | |
otherwise TypError will be raised.""" | |
if any(d < 0 for d in (width, height)): | |
raise TypeError("width and height must be two positive integer") | |
try: | |
self.surface = pygame.transform.smoothscale(self.surface, (width, height)) | |
except ValueError: | |
self.surface = pygame.transform.scale(self.surface, (width, height)) | |
self.rect.size = self.surface.get_rect().size | |
self.update_surround_rect() | |
def resize_from_rect (self, rect): | |
"""Resize at the size of the given rect.""" | |
self.resize (*rect.size) | |
def resize_perc_from (self, surface_or_rect, perc, dim='h'): | |
"""Resize the object at the given 'perc' relative to the | |
first args's 'surface_or_rect' size. | |
This scaling respect the surface proportions, so `perc` is relative | |
to the argument `dim`, which must be 'h' for the height or 'w' | |
for the width (If not provided, default to 'h'). | |
After the new size is computed, use self.resize to do the job.""" | |
try: | |
relative_rect = surface_or_rect.get_rect() | |
except AttributeError: | |
relative_rect = surface_or_rect | |
dims = namedtuple('dims', 'w h') | |
first_dim = getattr(relative_rect, dim) | |
setattr(dims, dim, first_dim * perc / 100) | |
other_dim = set(('w', 'h')).difference((dim,)).pop() | |
setattr(dims, other_dim, getattr(dims, dim) | |
* getattr(self.rect, other_dim) | |
/ getattr(self.rect, dim)) | |
self.resize(dims.w, dims.h) | |
def rotate (self, angle, anchor_at='center'): | |
"""Rotate the image by 'angle' amount. Should be a float value. | |
Negative angle amounts will rotate clockwise. | |
Use the attr _original_surface for rotation to avoid the surface | |
enlargement and (when many calls to rotate occurs) the consequently | |
segfault or raise of pygame exception. | |
""" | |
self.surface = self._original_surface | |
anchor_point = getattr(self.rect, anchor_at) | |
self.surface = pygame.transform.rotate(self.surface, angle) | |
#self.surface = pygame.transform.rotozoom(self.surface, angle, 1) | |
self.rect = self.surface.get_rect() | |
setattr(self.rect, anchor_at, anchor_point) | |
self.update_surround_rect() | |
def raise_actions (self, group='default'): | |
"""Execute the action(s) previously set. | |
'group' is a string, the name of the group which the action(s) | |
to execute belongs (defautl to 'default' group).""" | |
for action in self.actions[group]: | |
callable_, args, kwords = action | |
callable_(*args, **kwords) | |
def set_action (self, callable_, args=None, kwords=None, group='default'): | |
"""Set a new action. | |
'callable_' is a callable object which will be executed, the optional | |
argument 'args' must be a collection or args to pass to 'callable_' | |
and the optional argument 'kwords' a dict representing the callable's | |
keywords args. 'group' is the group which this action belongs to, if | |
not provided this action fall in the 'default' group. | |
""" | |
self.actions[group].append((callable_, args or [], kwords or {})) | |
def del_action_group (self, group): | |
"""Delete the action(s) of the group 'group' and return it. | |
Return None if 'group' is not present.""" | |
try: | |
return self.actions.pop(group) | |
except KeyError: | |
pass | |
def set_rect_attrs (self, dict_or_seq): | |
"""Set the object's rect attributes from the arg 'dict_or_seq'. | |
It can be a key:value mapping object (must provide an iteritems() | |
method) or any iterable of pairs (key, value). | |
""" | |
try: | |
items = dict_or_seq.iteritems() | |
except AttributeError: | |
items = dict_or_seq | |
for attr, value in items: | |
setattr(self.rect, attr, value) | |
self.update_surround_rect() | |
return self.rect | |
def set_surface (self, surface=None, resize=False): | |
"""change the object's surface with the first arg 'surface' and, | |
if the 'resize' arg is False (default) update its rect dimension | |
to fit the new surface; otherwise the new surface will be | |
resized to fit the old rect's size. | |
If surface is not provided, keep the actual surface. | |
This method also set the _original_surface attributes, a copy of | |
the object surface when just created. | |
""" | |
if not surface: | |
surface = self.surface | |
if resize: | |
old_x, old_y = self.rect.size | |
self.surface = surface | |
self.rect.size = self.surface.get_rect().size | |
self.update_surround_rect() | |
if resize: | |
self.resize(old_x, old_y) | |
self._original_surface = self.surface.copy() | |
def set_surround_rect (self, pixel=None, perc=None, rect=None): | |
"""Set the surround rect of the object's rect. | |
This is a dummy method which set some attribute used to track the | |
rect's changes and then call update_surround_rect' to perform | |
the resize. | |
'rect' must be a valid rect object; if present 'pixel' and 'perc' | |
values are ignored. | |
The 'pixel' optional arg must be an integer: the surround rect | |
will be resised by its value. | |
Otherwise, the 'perc' optional value can be used: the surround rect | |
will be resised - in percentage - by its value. | |
""" | |
if rect: | |
delta_w = rect.w - self.rect.w | |
delta_h = rect.h - self.rect.h | |
self.surround_rect_delta = (delta_w, delta_h) | |
elif not None in (pixel, perc): | |
raise TypeError("You must choose one between 'pixel' or 'perc'" | |
" to calculate the new rect size!") | |
elif pixel is not None: | |
self.surround_rect_delta = (pixel, pixel) | |
elif perc is not None: | |
w, h = self.rect.size | |
self.surround_rect_delta = ((perc * w / 100) - w, (perc * h / 100) - h) | |
self.update_surround_rect() | |
return self.surround_rect | |
def update_surround_rect (self): | |
# resizing with 'perc' don't change surround_rect in perc when update (FIXME: tomorrow) | |
"""Update the surround rect by the given delta perviously set | |
with the set_surround_rect method to be consistent with the object's | |
rect. This method is called any time the object's rect changes by | |
class method's call. | |
""" | |
self.surround_rect.size = map(sum, | |
zip(self.rect.size, self.surround_rect_delta)) | |
self.surround_rect.center = self.rect.center | |
# IMAGE CLASS | |
class Image(GenericGameObject): | |
"""An image class.""" | |
def __init__ (self, image_path_or_surface=None, cmp_value=None): | |
"""Set the object's surface as image_path_or_surface, | |
loading the image wich point to or just assign the surface. | |
""" | |
try: | |
surface = pygame.image.load( | |
image_path_or_surface.encode('utf-8')).convert_alpha() | |
except AttributeError: | |
surface = image_path_or_surface | |
super(Image, self).__init__(surface, cmp_value) | |
class TextImage (Image): | |
"""Like Image but used for create surfaces with text.""" | |
def __init__ (self, text, font_name, font_size, | |
text_color, bg_color=None, cmp_value=None): | |
"""Create a TextImage object: `text' is the text to be displayed; | |
`font_name' can be either a filename or a font name, in the latter | |
case the font must be present in the system, otherwise a random | |
available font is used instead; `font_size' is the initial size of | |
the font, may be change when the object resi change; `text_color' is | |
the color used when blit the text; `bg_color' is the background color | |
of the TextImage's surface (default to None, means transparent). | |
""" | |
self.font_name = font_name | |
self.font_size = font_size | |
self.text = str(text) | |
self.font = None | |
self.bg_color = bg_color | |
self.text_color = text_color | |
self._build_font(font_name, font_size) | |
super(TextImage, self).__init__(self._build_surface(), cmp_value or self.text) | |
def _build_surface(self): | |
"""Build the object's surface. Private method used in resizing | |
and object instantiation; see the docstring of the __init__ method. | |
""" | |
# XXX: pygame BUG (filled: http://pygame.motherhamster.org/bugzilla/show_bug.cgi?id=49 ) | |
# XXX: Also note that with this trick, an invalid bg_color args will | |
# cause the expressions in the except clause to be executed, so | |
# no exceptions will be raised (not a good thing, but that is). | |
try: | |
return self.font.render(self.text, True, self.text_color, self.bg_color) | |
except TypeError: | |
return self.font.render(self.text, True, self.text_color) | |
def _build_font(self, name, size): | |
"""Build the font. Private method used in resizing and object | |
instantiation; see the docstring of the __init__ method. | |
""" | |
self.font_name = name | |
self.font_size = size | |
try: | |
self.font = pygame.font.Font(self.font_name, self.font_size) | |
except IOError: | |
self.font = pygame.font.SysFont(self.font_name, self.font_size) | |
except RuntimeError: | |
self.font = pygame.font.SysFont( | |
','.join(pygame.font.get_fonts()), self.font_size) | |
def resize (self, w, h): | |
"""Resize the object at the given width and height, possibly | |
rebuilding the font and the object's surface. | |
""" | |
fw, fh = self.font.size(self.text) | |
if fh < h or fw < w: | |
self.font_size = max((self.font_size * h / fh, self.font_size * w / fw)) | |
self._build_font(self.font_name, self.font_size) | |
self.set_surface(self._build_surface(), False) # must be False. otherwise you sucks! | |
super(TextImage, self).resize(w, h) | |
# CELL CLASSES | |
class Cell (Image): | |
"""A Cell Class. used to create object that may contains | |
another game's object with some facilitations. | |
""" | |
def __init__ (self, image_path, item=None, cmp_value=None): | |
self.item = item | |
self.item_attrs = {} | |
self.u_surround_rect = pygame.Rect(0, 0, 0, 0) | |
super(Cell, self).__init__(image_path, cmp_value) | |
@property | |
def area (self): | |
"""The object's rect area.""" | |
return self.rect.width * self.rect.height | |
@property | |
def uarea (self): | |
"""The area of the union of the object's area and its item.""" | |
return rect_area(self.urect) | |
@property | |
def urect (self): | |
"""The union of the object's rect and its item's rect (if any).""" | |
return self.rect.union(self.item.rect) if self.item else self.rect | |
def add_item(self, item, dict_or_seq=None, draw=False): | |
"""Set the object's item. 'item' Must be a GenericGameObjects or compatible | |
objects. 'dict_or_seq' is a sequence of pairs of rect attrs by which 'item' | |
will be positioned in respect of the cell; if dict_or_seq is not provided, | |
the item's rect center will be the cell's rect center. | |
Use {rect_attr:None} to use the value of the cell rect. Also You can pass, | |
for example, {'left':'top'} to set the 'left' attribute of 'item' | |
with the value of the 'top' attribute of the cell rect. | |
If 'draw' is a true value, the item will be draw on the cell's surface. | |
This object can contain only one item at a time. Multiple calls of | |
'add_item' cause the deletion of the previously item (if any). | |
""" | |
self.item = item | |
self.item_attrs = dict_or_seq or {'center':None} | |
self.update_item() | |
if draw: | |
self.item.draw_on(self.surface) | |
return self.item.rect | |
def draw_on(self, surface, background=None, dict_or_seq=None, draw_item=False): | |
"""Draw the objects on `surface'. Same as the GenericGameObject's | |
draw_on method, but also permit to blit the object's item if | |
`draw_item' is set to a true value. | |
""" | |
rect = super(Cell, self).draw_on(surface, background, dict_or_seq) | |
if draw_item: | |
return (rect, self.item.draw_on(self.surface)) | |
return rect | |
def move_bouncing (self, bounce_rect, velocity=(None, None)): | |
"""Move the object's rect bouncing inside 'bounce_rect' | |
by 'velocity' (a tuple, default to self.velocity). | |
""" | |
x, y = velocity if any(velocity) else self.velocity | |
new_velocity = [x, y] | |
rect = self.urect | |
if (rect.left + x <= bounce_rect.left or | |
rect.right + x >= bounce_rect.right): | |
new_velocity[0] = -x | |
if (rect.bottom + y >= bounce_rect.bottom or | |
rect.top + y <= bounce_rect.top): | |
new_velocity[1] = -y | |
self.move_ip(x, y) | |
self.clamp_ip(bounce_rect) | |
self.velocity = new_velocity | |
return self.rect | |
def update_item(self): | |
"""Update the object's item (if any) following the | |
changes in the object. | |
""" | |
try: | |
kv = self.item_attrs.iteritems() | |
except AttributeError: | |
kv = self.item_attrs | |
for k, v in kv: | |
try: | |
setattr(self.item.rect, k, v) | |
except (SystemError, TypeError): | |
if v is None: | |
setattr(self.item.rect, k, getattr(self.rect, k)) | |
else: | |
try: | |
setattr(self.item.rect, k, getattr(self.rect, v)) | |
except TypeError: | |
setattr(self.item.rect, k, v()) | |
def update_surround_rect (self): | |
"""Update the surround rect(s), both the object's surround rect | |
and the object+item union's surround rect. | |
""" | |
super(Cell, self).update_surround_rect() | |
self.update_item() | |
self.u_surround_rect.size = map(sum, zip( | |
self.urect.size, self.surround_rect_delta)) | |
self.u_surround_rect.center = self.rect.center | |
# GRID RELATED CLASS | |
class Grid (GenericGameObject): | |
"""Class to create Grid of objects. | |
Derived from GenericGameObject, from which take some method, | |
but override others to work in a very different manner. | |
""" | |
def __init__ (self, fill_with=None, fill_dict_args=None): | |
super(Grid, self).__init__() | |
self.ncolumns = 0 | |
self.nrows = 0 | |
self._grid = [] | |
self._row_position = 0 | |
self._col_position = 0 | |
self._current_coord = None | |
self.fill_object = fill_with | |
self.fill_object_args = fill_dict_args or dict() | |
def __contains__ (self, item): | |
for obj in self: | |
if obj == item: | |
self.rewind() | |
return True | |
return False | |
def __getitem__ (self, item): | |
try: | |
row_pos, col_pos = divmod(item, self.ncolumns) | |
return self._grid[row_pos][col_pos] | |
except TypeError: | |
try: | |
row, col = item | |
return self._grid[row][col] | |
except TypeError: | |
return self._grid[item] | |
except ValueError, err: | |
raise ValueError("%s (item: %s %s)" % (err, repr(item), type(item))) | |
except IndexError, err: | |
raise IndexError("%s %s (on grid of %dX%d)" | |
% (err, item, self.nrows, self.ncolumns)) | |
def __iter__ (self): | |
return self | |
def __len__ (self): | |
return sum(len(row) for row in self._grid) | |
def __setitem__ (self, item, value): | |
try: | |
row_pos, col_pos = divmod(item, self.ncolumns) | |
self._grid[row_pos][col_pos] = value | |
except TypeError: | |
try: | |
row, col = item | |
self._grid[row][col] = value | |
except ValueError, err: | |
raise ValueError("%s %s" % (err, item)) | |
except IndexError, err: | |
raise IndexError("%s %s (on grid of %dX%d)" | |
% (err, item, self.nrows, self.ncolumns)) | |
def __str__ (self): | |
return "Grid object <%s>" % ','.join(str(r) for r in self) | |
@property | |
def rows (self): | |
"""The rows of the grid, as a list of lists.""" | |
return self[:] | |
@property | |
def columns (self): | |
"""The columns of the grid, as a list of lists.""" | |
nrows = range(self.nrows) | |
return [[self[row, col] for row in nrows] for col in range(self.ncolumns)] | |
@property | |
def current_coord (self): | |
"""The current position on the grid, the two values (row, col).""" | |
return self._current_coord | |
def build (self, ncolumns, nrows, cell_size): | |
"""Build a nrowsXncolumns grid, with cells of size `cell_size'.""" | |
self._grid = [] | |
self.ncolumns = ncolumns | |
self.nrows = nrows | |
if not self.fill_object: | |
self._grid = [[None for c in range(self.ncolumns)] | |
for r in range(self.nrows)] | |
self._current_coord = self._row_position, self._col_position | |
return | |
_topleft = self.rect.topleft | |
for row in range(self.nrows): | |
cells = [self.fill_object(**self.fill_object_args) | |
for column in range(self.ncolumns)] | |
for cell in cells: | |
cell.resize(*cell_size) | |
cell.set_rect_attrs({'size':cell_size, 'topleft': _topleft}) | |
_topleft = cell.rect.topright | |
self.rect.union_ip(cell) | |
_topleft = cells[0].rect.bottomleft | |
self._grid.append(cells) | |
self._current_coord = self._row_position, self._col_position | |
self.surround_rect = copy_rect(self.rect) | |
def draw_on (self, surface, background=None): | |
"""Draw the grid on `surface', return a iterator of its | |
cells' rects (intended to be used in display.update). | |
""" | |
return (cell.draw_on(surface) for cell in self) | |
def next (self): | |
try: | |
item = self[self._row_position, self._col_position] | |
self._current_coord = self._row_position, self._col_position | |
self._col_position += 1 | |
return item | |
except IndexError: | |
if self._row_position >= self.nrows-1: | |
self._row_position = self._col_position = 0 | |
self._current_coord = self._row_position, self._col_position | |
raise StopIteration | |
elif self._col_position >= self.ncolumns-1: | |
self._row_position += 1 | |
self._col_position = 0 | |
try: | |
item = self[self._row_position, self._col_position] | |
self._current_coord = self._row_position, self._col_position | |
self._col_position += 1 | |
return item | |
except IndexError: | |
self._row_position = self._col_position = 0 | |
self._current_coord = self._row_position, self._col_position | |
raise StopIteration | |
def rewind (self): | |
"""Back to `current_coord' (0,0), the first item of the grid. | |
Can be used between iterations on the grid, when starting with | |
a 'fresh' iterator is needed. | |
""" | |
self._row_position = self._col_position = 0 | |
self._current_coord = (0, 0) | |
def move (self, new_x=0, new_y=0): | |
"""Move the grid's topleft corner at (new_x, new_y).""" | |
_topleft = map(sum, zip(self.rect.topleft, (new_x, new_y))) | |
self.rect.topleft = _topleft | |
for row in self.rows: | |
for cell in row: | |
cell.set_rect_attrs({'topleft': _topleft}) | |
_topleft = cell.rect.topright | |
self.rect.union_ip(cell.rect) | |
_topleft = row[0].rect.bottomleft | |
self.update_surround_rect() | |
def resize (self, new_x=None, new_y=None): | |
"""Resize the grid and its cells to the new (new_x, new_y) size.""" | |
x = new_x if new_x else self.rect.width | |
y = new_y if new_y else self.rect.height | |
cell_size = (x / self.ncolumns, y / self.nrows) | |
if not all(cell_size): | |
raise ValueError("Can't resize this! Too few space and too much cells.") | |
_topleft = self.rect.topleft | |
self.rect = pygame.Rect(_topleft, (0, 0)) | |
for row in self.rows: | |
for cell in row: | |
cell.resize(*cell_size) | |
cell.set_rect_attrs({'size':cell_size, 'topleft': _topleft}) | |
_topleft = cell.rect.topright | |
self.rect.union_ip(cell.rect) | |
_topleft = row[0].rect.bottomleft | |
self.update_surround_rect() | |
def rotate (self, step): | |
"""Rotate the cells' position in the grid by `step' | |
(can be negative). | |
""" | |
max_ = len(self) | |
step = step if ((-max_) <= step <= max_) else step % max_ | |
old = [x for x in self] | |
l = old[:step] | |
ll = old[step:] | |
old[len(l):] = l | |
old[:len(l)] = ll | |
for pos, val in enumerate(old): | |
self[pos] = val | |
self.update_surround_rect() | |
def shuffle (self): | |
"""Shuffle the cells in the grid.""" | |
_len = len(self)-1 | |
for i in range(self.ncolumns * self.nrows): | |
old_cell, new_cell = [random.randint(0, _len) for dim in 'rc'] | |
if old_cell == new_cell: | |
continue | |
old_rect = copy_rect(self[old_cell].rect) | |
new_rect = copy_rect(self[new_cell].rect) | |
self[old_cell], self[new_cell] = self[new_cell], self[old_cell] | |
self[old_cell].rect = old_rect | |
self[new_cell].rect = new_rect | |
self.update_surround_rect() | |
def update (self): | |
"""To call after the grid has been filled with new object | |
without the build() method, and if cells need position adjustment. | |
for ensuring cell alignment. Actually this method does nothing but | |
calling move() with zero values for the new position.""" | |
self.move() | |
class MemoryGrid (Grid): | |
"""Specifiv Grid class for Memory game.""" | |
def __init__ (self, ncolumns, nrows, cell_size): | |
super(MemoryGrid, self).__init__() | |
self.build(ncolumns, nrows, cell_size) | |
self.cover = None | |
def draw_on_covered (self, surface, cells=None): | |
"""Draw the grid's cover surface on `surface'. | |
If `¢ell' is provided, must be a container filled with | |
games' or compatible objects; in this case blit them | |
on `surface' nstead of the grid's cells. | |
Return the list of rects just blitted. | |
""" | |
rects = [] | |
for cell in (cells or self): | |
rects.append(surface.blit(self.cover.surface, cell.rect)) | |
return rects | |
def set_cover (self, image_path): | |
"""set the grid's cells cover as an Image object from | |
`image_path', which must be a path to the target image or a | |
pygame.Surface object. If the grid is already filled with | |
game's objects, the cover will be resized at the size of the | |
first cell. | |
""" | |
self.cover = Image(image_path) | |
if self[0]: | |
self.cover.resize(*self[0].rect.size) | |
class GridCell (Image): | |
"""Class which provide some convenient method for managing | |
MemoryGrid's cell objects. | |
""" | |
def __init__ (self, image_path=None, cmp_value=None): | |
super(GridCell, self).__init__(image_path, cmp_value) | |
self._covered = True | |
@property | |
def covered (self): | |
"""Return True id the cell is covered.""" | |
return self._covered | |
def toggle (self): | |
"""Toggle the cell's covered state.""" | |
self._covered ^= True | |
class MovingTextButton(TextImage): | |
"""TextImage Class with some other methods | |
for simplify object's movements. | |
""" | |
def __init__(self, *args, **kword): | |
super(MovingTextButton, self).__init__(*args, **kword) | |
self.start_position = self.rect.center | |
def update_start_position (self): | |
"""Set the object's `start_position' at its actual rect's center.""" | |
self.start_position = self.rect.center | |
def goto_start(self): | |
"""Set the object's rect center at its `start_position' value.""" | |
self.rect.center = self.start_position | |
self.update_surround_rect() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment