# A library file for simplifying pygame interaction.
Required: you must download gamebox.py to use the other files here.
'''This code is the original work of Luther Tychonievich, who releases it
into the public domain.
As a courtesy, Luther would appreciate it if you acknowledged him in any work
that benefited from this code.'''
from __future__ import division
import pygame, sys
import urllib, os.path
if 'urlretrieve' not in dir(urllib):
from urllib.request import urlretrieve as _urlretrieve
else:
_urlretrieve = urllib.urlretrieve
pygame.init()
# a cache to avoid loading images many time
_known_images = {}
_known_sounds = {}
def _image(key, flip=False, w=0, h=0, angle=0):
'''A method for loading images, caching them, and flipping them'''
if '__hash__' not in dir(key):
key = id(key)
angle,w,h = int(angle), int(w), int(h)
ans = None
if (key,flip,w,h,angle) in _known_images:
ans = _known_images[(key,flip,w,h,angle)]
elif angle != 0:
base = _image(key,flip,w,h)
img = pygame.transform.rotozoom(base, angle, 1)
_known_images[(key,flip,w,h,angle)] = img
ans = img
elif w != 0 or h != 0:
base = _image(key, flip)
img = pygame.transform.smoothscale(base, (w,h))
_known_images[(key,flip,w,h,angle)] = img
ans = img
elif flip:
base = _image(key)
img = pygame.transform.flip(base, True, False)
_known_images[(key,flip,w,h,angle)] = img
ans = img
else:
img, _ = _get_image(key)
_known_images[(key,flip,w,h,angle)] = img
ans = img
if w == 0 and h == 0:
if angle != 0: tmp = _image(key, flip, w, h)
else: tmp = ans
_known_images[(key,flip,tmp.get_width(),tmp.get_height(), angle)] = ans
return ans
def _image_from_url(url):
'''a method for loading images from urls by first saving them locally'''
filename = os.path.basename(url)
if not os.path.exists(filename):
if '://' not in url: url = 'http://'+url
_urlretrieve(url, filename)
image, filename =_image_from_file(filename)
return image, filename
def _image_from_file(filename):
'''a method for loading images from files'''
image = pygame.image.load(filename).convert_alpha()
_known_images[filename] = image
_known_images[(image.get_width(), image.get_height(), filename)] = image
return image, filename
def _get_image(thing):
'''a method for loading images from cache, then file, then url'''
if thing in _known_images: return _known_images[thing], thing
sid = '__id__'+str(id(thing))
if sid in _known_images: return _known_images[sid], sid
if type(thing) is str:
if os.path.exists(thing): return _image_from_file(thing)
return _image_from_url(thing)
_known_images[sid] = thing
_known_images[(thing.get_width(), thing.get_height(), sid)] = thing
return thing, sid
def load_sprite_sheet(url_or_filename, rows, columns):
'''Loads a sprite sheet. Assumes the sheet has rows-by-columns evenly-spaced images and returns a list of those images.'''
sheet, key = _get_image(url_or_filename)
height = sheet.get_height() / rows
width = sheet.get_width() / columns
frames = []
for row in range(rows):
for col in range(columns):
clip = pygame.Rect( col*width, row*height, width, height )
frame = sheet.subsurface(clip)
frames.append(frame)
return frames
__all__ = ['load_sprite_sheet']
def from_image(x, y, filename_or_url):
'''Creates a SpriteBox object at the given location from the provided filename or url'''
image, key = _get_image(filename_or_url)
return SpriteBox(x, y, image, None)
__all__.append('from_image')
def from_color(x, y, color, width, height):
'''Creates a SpriteBox object at the given location with the given color, width, and height'''
return SpriteBox(x, y, None, color, width, height)
__all__.append('from_color')
def from_text(x, y, text, fontname, fontsize, color, bold=False, italic=False):
'''Creates a SpriteBox object at the given location with the given text as its content'''
font = pygame.font.match_font(fontname.replace(" ","").lower())
if font is None:
sys.stderr.write("ERROR: no font named "+fontname+"; using default font instead")
font = pygame.font.Font(font,fontsize)
font.set_bold(bold)
font.set_italic(italic)
if type(color) is str: color = pygame.Color(color)
return from_image(x,y,font.render(text,True,color))
__all__.append('from_text')
def load_sound(url_or_filename):
'''Reads a sound file from a given filename or url'''
if url_or_filename in _known_images: return _known_sounds[url_or_filename]
if not os.path.exists(url_or_filename):
filename = os.path.basename(url_or_filename)
if not os.path.exists(filename):
_urlretrieve(url_or_filename, filename)
url_or_filename = filename
sound = pygame.mixer.Sound(url_or_filename)
_known_sounds[url_or_filename] = sound
return sound
__all__.append('load_sound')
class Camera(object):
'''A camera defines what is visible. It has a width, height, full screen status,
and can be moved. Moving a camera changes what is visible.
'''
is_initialized = False
# __slots__ = ["_surface", "x", "y", "speedx", "speedy"]
def __init__(self, width, height, full_screen=False):
'''Camera(pixelsWide, pixelsTall, False) makes a window; using True instead makes a full-screen display.'''
if Camera.is_initialized: raise Exception("You can only have one Camera at a time")
if height > 768: raise Exception("The Game Expo screens will only be 768 pixels tall")
if width > 1366: raise Exception("The Game Expo screens will only be 1366 pixels wide")
if full_screen:
self.__dict__['_surface'] = pygame.display.set_mode([width, height], pygame.FULLSCREEN)
else:
self.__dict__['_surface'] = pygame.display.set_mode([width, height])
self.__dict__['_x'] = 0
self.__dict__['_y'] = 0
Camera.is_initialized = True
def move(self, x, y=None):
'''camera.move(3, -7) moves the screen's center to be 3 more pixels to the right and 7 more up'''
if y is None: x, y = x
self.x += x
self.y += y
def draw(self, thing, *args):
'''camera.draw(box) draws the provided SpriteBox object
camera.draw(image, x, y) draws the provided image centered at the provided coordinates
camera.draw("Hi", "Arial", 12, "red", x, y) draws the text Hi in a red 12-point Arial font at x,y'''
if isinstance(thing, SpriteBox):
thing.draw(self)
elif isinstance(thing, pygame.Surface):
try:
if len(args) == 1: x,y = args[0]
else: x,y = args[:2]
self._surface.blit(thing, [x-thing.get_width()/2,y-thing.get_height()/2])
except e:
raise Exception("Wrong arguments; try .draw(surface, [x,y])")
elif type(thing) is str:
try:
font = pygame.font.match_font(args[0].replace(" ","").lower())
if font is None:
sys.stderr.write("ERROR: no font named "+fontname+"; using default font instead")
size = args[1]
color = args[2]
if type(color) is str: color = pygame.Color(color)
self.draw(pygame.font.Font(font,size).render(thing,True,color), *args[3:])
except e:
raise Exception("Wrong arguments; try .draw(text, fontName, fontSize, color, [x,y])")
else:
raise Exception("I don't know how to draw a ",type(thing))
def display(self):
'''Causes what has been drawn recently by calls to draw(...) to be displayed on the screen'''
pygame.display.flip()
def clear(self, color):
'''Erases the screen by filling it with the given color'''
if type(color) is str: color = pygame.Color(color)
self._surface.fill(color)
def __getattr__(self, name):
if name in self.__dict__: return self.__dict__[name]
x, y, w, h = self._x, self._y, self._surface.get_width(), self._surface.get_height()
if name == 'left': return x
if name == 'right': return x + w
if name == 'top': return y
if name == 'bottom': return y + h
if name == 'x': return x + w/2
if name == 'y': return y + h/2
if name == 'center': return x+w/2, y+h/2
if name == 'topleft': return x,y
if name == 'topright': return x + w, y
if name == 'bottomleft': return x, y + h
if name == 'bottomright': return x + w, y + h
if name == 'width': return w
if name == 'height': return h
if name == 'size': return w, h
if name == 'mousex': return pygame.mouse.get_pos()[0] + self._x
if name == 'mousey': return pygame.mouse.get_pos()[1] + self._y
if name == 'mouse': return pygame.mouse.get_pos()[0] + self._x, pygame.mouse.get_pos()[1] + self._y
if name == 'mouseclick': return any(pygame.mouse.get_pressed())
raise Exception("There is no '" + name + "' in a Camera object")
def __setattr__(self, name, value):
if name in self.__dict__:
self.__dict__[name] = value
return
w, h = self._surface.get_width(), self._surface.get_height()
if name == 'left': self._x = value
elif name == 'right': self._x = value - w
elif name == 'top': self._y = value
elif name == 'bottom': self._y = value - h
elif name == 'x': self._x = value-w/2
elif name == 'y': self._y = value-h/2
elif name == 'center': self._x, self._y = value[0]-w/2, value[1]-h/2
elif name == 'topleft': self._x, self._y = value[0], value[1]
elif name == 'topright': self._x, self._y = value[0] - w, value[1]
elif name == 'bottomleft': self._x, self._y = value[0], value[1] - h
elif name == 'bottomright': self._x, self._y = value[0] - w, value[1] - h
elif name in ['width','height','size','mouse','mousex','mousey','mouseclick']:
raise Exception("You cannot change the '" + name + "' of a Camera object")
else:
sys.stderr.write("creating field named "+name)
self.__dict__[name] = value
def __repr__(self):
return str(self)
def __str__(self):
return '%dx%d Camera centered at %d,%d' % (self.width, self.height, self.x, self.y)
__all__.append('Camera')
class SpriteBox(object):
'''Intended to represent a sprite (i.e., an image that can be drawn as part of a larger view) and the box that contains it. Has various collision and movement methods built in.'''
# __slots__ = ["x","y","speedx","speedy","_w","_h","_key","_image","_color"]
def __init__(self, x, y, image, color, w=None, h=None):
'''You should probably use the from_image, from_text, or from_color method instead of this one'''
self.__dict__['x'] = x
self.__dict__['y'] = y
self.__dict__['speedx'] = 0
self.__dict__['speedy'] = 0
if image is not None:
self._set_key(image, False, 0, 0, 0)
if w is not None:
if h is not None: self.size = w,h
else: self.width = w
elif h is not None: self.height = h
elif color is not None:
if w is None or h is None: raise Exception("must supply size of color box")
self.__dict__['_key'] = None
self.__dict__['_image'] = None
self.__dict__['_w'] = w
self.__dict__['_h'] = h
self.color = color
pass
def _set_key(self, name, flip, width, height, angle):
width = int(width+0.5)
height = int(height+0.5)
angle = ((int(angle)%360)+360)%360
unrot = _image(name, flip, width, height)
if width == 0 and height == 0:
width = unrot.get_width()
height = unrot.get_height()
self.__dict__['_key'] = (name, flip, width, height, angle)
self.__dict__['_image'] = _image(*self.__dict__['_key'])
self.__dict__['_color'] = None
self.__dict__['_w'] = self.__dict__['_image'].get_width()
self.__dict__['_h'] = self.__dict__['_image'].get_height()
def __getattr__(self, name):
x, y, w, h = self.x, self.y, self._w, self._h
if name == 'xspeed': name = 'speedx'
if name == 'yspeed': name = 'speedy'
if name == 'left': return x - w / 2
if name == 'right': return x + w / 2
if name == 'top': return y - h / 2
if name == 'bottom': return y + h / 2
if name == 'center': return x, y
if name == 'topleft': return x - w / 2, y - h / 2
if name == 'topright': return x + w / 2, y - h / 2
if name == 'bottomleft': return x - w / 2, y + h / 2
if name == 'bottomright': return x + w / 2, y + h / 2
if name == 'width': return w
if name == 'height': return h
if name == 'width': return w
if name == 'height': return h
if name == 'size': return w, h
if name == 'speed': return self.speedx, self.speedy
if name == 'rect': return pygame.Rect(self.topleft, self.size)
if name == 'image': return self.__dict__['_image']
if name in self.__dict__:
return self.__dict__[name]
raise Exception("There is no '" + name + "' in a SpriteBox object")
def __setattr__(self, name, value):
w, h = self._w, self._h
if name == 'xspeed': name = 'speedx'
if name == 'yspeed': name = 'speedy'
if name in self.__dict__:
self.__dict__[name] = value
elif name == 'left': self.x = value + w / 2
elif name == 'right': self.x = value - w / 2
elif name == 'top': self.y = value + h / 2
elif name == 'bottom': self.y = value - h / 2
elif name == 'center': self.x, self.y = value[0], value[1]
elif name == 'topleft': self.x, self.y = value[0] + w / 2, value[1] + h / 2
elif name == 'topright': self.x, self.y = value[0] - w / 2, value[1] + h / 2
elif name == 'bottomleft': self.x, self.y = value[0] + w / 2, value[1] - h / 2
elif name == 'bottomright': self.x, self.y = value[0] - w / 2, value[1] - h / 2
elif name == 'width': self.scale_by(value/w)
elif name == 'height': self.scale_by(value/h)
elif name == 'size':
if self.__dict__['_image'] is not None:
key = self.__dict__['_key']
self._set_key(key[0], key[1], value[0], value[1], key[4])
else:
self.__dict__['_w'] = value[0]
self.__dict__['_h'] = value[1]
elif name == 'speed': self.speedx, self.speedy = value[0], value[1]
elif name == 'color':
self.__dict__['_image'] = None
self.__dict__['_key'] = None
if type(value) is str: value = pygame.Color(value)
self.__dict__['_color'] = value
elif name == 'image':
self.__dict__['_color'] = None
if self.__dict__['_key'] is None:
self._set_key(value, False, w, h, 0)
else:
key = self.__dict__['_key']
self._set_key(value, *key[1:])
else:
sys.stderr.write("creating filed named "+name)
self.__dict__[name] = value
def overlap(self, other, padding=0, padding2=None):
'''b1.overlap(b1) returns a list of 2 values such that self.move(result) will cause them to not overlap
Returns [0,0] if there is no overlap (i.e., if b1.touches(b2) returns False
b1.overlap(b2, 5) adds a 5-pixel padding to b1 before computing the overlap
b1.overlap(b2, 5, 10) adds a 5-pixel padding in x and a 10-pixel padding in y before computing the overlap'''
if padding2 is None: padding2 = padding
l = other.left - self.right - padding
r = self.left - other.right - padding
t = other.top - self.bottom - padding2
b = self.top - other.bottom - padding2
m = max(l, r, t, b)
if m >= 0: return [0, 0]
elif m == l: return [l, 0]
elif m == r: return [-r, 0]
elif m == t: return [0, t]
else: return [0, -b]
def touches(self, other, padding=0, padding2=None):
'''b1.touches(b1) returns True if the two SpriteBoxes overlap, False if they do not
b1.touches(b2, 5) adds a 5-pixel padding to b1 before computing the touch
b1.touches(b2, 5, 10) adds a 5-pixel padding in x and a 10-pixel padding in y before computing the touch'''
if padding2 is None: padding2 = padding
l = other.left - self.right - padding
r = self.left - other.right - padding
t = other.top - self.bottom - padding2
b = self.top - other.bottom - padding2
return max(l,r,t,b) <= 0
def bottom_touches(self, other, padding=0, padding2=None):
'''b1.bottom_touches(b2) returns True if both b1.touches(b2) and b1's bottom edge is the one causing the overlap.'''
if padding2 is None: padding2 = padding
return self.overlap(other,padding+1,padding2+1)[1] < 0
def top_touches(self, other, padding=0, padding2=None):
'''b1.top_touches(b2) returns True if both b1.touches(b2) and b1's top edge is the one causing the overlap.'''
if padding2 is None: padding2 = padding
return self.overlap(other,padding+1,padding2+1)[1] > 0
def left_touches(self, other, padding=0, padding2=None):
'''b1.left_touches(b2) returns True if both b1.touches(b2) and b1's left edge is the one causing the overlap.'''
if padding2 is None: padding2 = padding
return self.overlap(other,padding+1,padding2+1)[0] > 0
def right_touches(self, other, padding=0, padding2=None):
'''b1.right_touches(b2) returns True if both b1.touches(b2) and b1's right edge is the one causing the overlap.'''
if padding2 is None: padding2 = padding
return self.overlap(other,padding+1,padding2+1)[0] < 0
def contains(self, x, y=None):
'''checks if the given point is inside this SpriteBox's bounds or not'''
if y is None: x,y = x
return abs(x-self.x)*2 < self._w and abs(y-self.y)*2 < self._h
def move_to_stop_overlapping(self, other, padding=0, padding2=None):
'''b1.move_to_stop_overlapping(b2) makes the minimal change to b1's position necessary so that they no longer overlap'''
o = self.overlap(other,padding, padding2)
if o != [0,0]:
self.move(o)
if o[0] * self.speedx < 0: self.speedx = 0
if o[1] * self.speedy < 0: self.speedy = 0
def move_both_to_stop_overlapping(self, other, padding=0, padding2=None):
'''b1.move_both_to_stop_overlapping(b2) changes both b1 and b2's positions so that they no longer overlap'''
o = self.overlap(other,padding, padding2)
if o != [0,0]:
self.move(o[0]/2,o[1]/2)
other.move(-o[0]/2,-o[1]/2)
if o[0] != 0:
self.speedx = (self.speedx+other.speedx)/2
other.speedx = self.speedx
if o[1] != 0:
self.speedy = (self.speedy+other.speedy)/2
other.speedy = self.speedy
def move(self, x, y=None):
'''change position by the given amount in x and y. If only x given, assumed to be a point [x,y]'''
if y is None: x, y = x
self.x += x
self.y += y
def move_speed(self):
'''change position by the current speed field of the SpriteBox object'''
self.move(self.speedx, self.speedy)
def full_size(self):
'''change size of this SpriteBox to be the original size of the source image'''
if self.__dict__['_key'] is None: return
key = self.__dict__['_key']
self._set_key(key[0],key[1],0,0,key[4])
def __repr__(self):
return str(self)
def __str__(self):
return '%dx%d SpriteBox centered at %d,%d' % (self._w, self._h, self.x, self.y)
def copy_at(self, newx, newy):
'''Make a new SpriteBox just like this one but at the given location instead of here'''
return SpriteBox(newx, newy, self._image, self._color, self._w, self._h)
def copy(self):
'''Make a new SpriteBox just like this one and in the same location'''
return self.copy_at(self.x, self.y)
def scale_by(self, multiplier):
'''Change the size of this SpriteBox by the given factor
b1.scale_by(1) does nothing; b1.scale_by(0.4) makes b1 40% of its original width and height.'''
if self.__dict__['_key'] is None:
self._w *= multiplier
self._h *= multiplier
else:
key = self.__dict__['_key']
self._set_key(key[0], key[1], key[2]*multiplier, key[3]*multiplier, key[4])
def draw(self, surface):
'''b1.draw(camera) is the same as saying camera.draw(b1)
b1.draw(image) draws a copy of b1 on the image proivided'''
if isinstance(surface, Camera):
if self.__dict__['_color'] is not None:
region = self.rect.move(-surface._x, -surface._y)
region = region.clip(surface._surface.get_rect())
surface._surface.fill(self._color, region)
elif self.__dict__['_image'] is not None:
surface._surface.blit(self._image, [self.left - surface._x, self.top - surface._y])
else:
if self.__dict__['_color'] is not None:
surface.fill(self._color, self.rect)
elif self.__dict__['_image'] is not None:
surface.blit(self._image, self.topleft)
def flip(self):
'''mirrors the SpriteBox left-to-right.
Mirroring top-to-bottom can be accomplished by
b1.rotate(180)
b1.flip()'''
if self.__dict__['_key'] is None: return
key = self.__dict__['_key']
self._set_key(key[0], not key[1], *key[2:])
def rotate(self, angle):
'''Rotates the SpriteBox by the given angle (in degrees).'''
if self.__dict__['_key'] is None: return
key = self.__dict__['_key']
self._set_key(key[0], key[1], key[2], key[3], key[4]+angle)
_timeron = False
_timerfps = 0
def timer_loop(fps, callback):
'''Requests that pygame call the provided function fps times a second
fps: a number between 1 and 60
callback: a function that accepts a set of keys pressed since the last tick
----
seconds = 0
def tick(keys):
seconds += 1/30
if pygame.K_DOWN in keys:
print 'down arrow pressed'
if not keys:
print 'no keys were pressed since the last tick'
camera.draw(box)
camera.display()
gamebox.timer_loop(30, tick)
----'''
global _timeron, _timerfps
keys = set([])
if fps > 1000: fps = 1000
_timerfps = fps
_timeron = True
pygame.time.set_timer(pygame.USEREVENT, int(1000/fps))
while True:
event = pygame.event.wait()
if event.type == pygame.QUIT: break
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: break
if event.type == pygame.KEYDOWN:
keys.add(event.key)
if event.type == pygame.KEYUP and event.key in keys:
keys.remove(event.key)
if event.type == pygame.USEREVENT:
pygame.event.clear(pygame.USEREVENT)
callback(keys)
pygame.time.set_timer(pygame.USEREVENT, 0)
_timeron = False
def pause():
'''Pauses the timer; an error if there is no timer to pause'''
if not _timeron: raise Exception("Cannot pause a timer before calling timer_loop(fps, callback)")
pygame.time.set_timer(pygame.USEREVENT, 0)
def unpause():
'''Unpauses the timer; an error if there is no timer to unpause'''
if not _timeron: raise Exception("Cannot pause a timer before calling timer_loop(fps, callback)")
pygame.time.set_timer(pygame.USEREVENT, int(1000/_timerfps))
def stop_loop():
'''Completely quits one timer_loop or keys_loop, usually ending the program'''
pygame.event.post(pygame.event.Event(pygame.QUIT))
def keys_loop(callback):
'''Requests that pygame call the provided function each time a key is pressed
callback: a function that accepts the key pressed
----
def onPress(key):
if pygame.K_DOWN == key:
print 'down arrow pressed'
if pygame.K_a in keys:
print 'A key pressed'
camera.draw(box)
camera.display()
gamebox.keys_loop(onPress)
----'''
while True:
event = pygame.event.wait()
if event.type == pygame.QUIT: break
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: break
if event.type == pygame.KEYDOWN:
callback(event.key)
if __name__ == "__main__":
camera = Camera(400, 400)
camera.x = 10
b = from_text(40,50,"Blue","Arial",40,"red", italic=True, bold=True)
b.speedx = 3
b.left += 2
b.y = 100
b.move_speed()
camera.draw(b)
camera.display()
smurfs = load_sprite_sheet("http://roussinavalkova.com/blog/wp-content/uploads/2011/04/smurf_sprite.png", 4, 4)
music = load_sound("http://www.gnu.org/fun/jokes/eternal-flame.ogg")
sound = music.play()
def tick(keys):
if keys:
if pygame.K_0 in keys: b.image = smurfs[0]
elif pygame.K_1 in keys: b.image = smurfs[1]
elif pygame.K_2 in keys: b.image = smurfs[2]
elif pygame.K_3 in keys: b.image = smurfs[3]
elif pygame.K_4 in keys: b.image = smurfs[4]
elif pygame.K_5 in keys: b.image = smurfs[5]
elif pygame.K_6 in keys: b.image = smurfs[6]
elif pygame.K_7 in keys: b.image = smurfs[7]
elif pygame.K_8 in keys: b.image = smurfs[8]
elif pygame.K_9 in keys: b.image = smurfs[9]
elif pygame.K_a in keys: stop_loop()
elif keys: b.image = "http://www.pygame.org/docs/_static/pygame_tiny.png"
b.full_size()
b.rotate(-5)
b.center = camera.mouse
b.bottom = camera.bottom
camera.draw(b)
camera.display()
timer_loop(30, tick)