Skip to content

Instantly share code, notes, and snippets.

@crap0101
Created June 9, 2011 03:30
Show Gist options
  • Save crap0101/1015994 to your computer and use it in GitHub Desktop.
Save crap0101/1015994 to your computer and use it in GitHub Desktop.
Conway's Game of Life with pygame
# -*- 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)
#!/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