|
Programmer's Notebook |
All computer source code presented on this page, unless it includes attribution to another author, is provided by Ed Halley under the Artistic License. Use such code freely and without any expectation of support. I would like to know if you make anything cool with the code, or need questions answered.
python/ bindings.py boards.py buzz.py cache.py cards.py constraints.py csql.py english.py getopts.py gizmos.py goals.py improv.py interpolations.py namespaces.py nihongo.py nodes.py octalplus.py patterns.py persist.py physics.py pids.py pieces.py quizzes.py recipes.py relays.py romaji.py ropen.py sheets.py strokes.py subscriptions.py svgbuild.py testing.py things.py timing.py ucsv.py useful.py uuid.py vectors.py weighted.py java/ CSVReader.java CSVWriter.java GlobFilenameFilter.java RegexFilenameFilter.java StringBufferOutputStream.java ThreadSet.java Throttle.java TracingThread.java Utf8ConsoleTest.java droid/ ArrangeViewsTouchListener.java DownloadFileTask.java perl/ CVQM.pm Kana.pm Typo.pm cxx/ CCache.h equalish.cpp |
Download pieces.py
|
# pieces - a library for making game pieces for board games using pygame ''' A set of common table-top game pieces for use in the "things" framework. AUTHOR Ed Halley (ed@halley.cc) 6 November 2007 ''' import os import math import random import pygame ; from pygame.locals import * import vectors ; from vectors import * import things ; from things import * import goals ; from goals import * import strokes ; from strokes import * #---------------------------------------------------------------------------- class Piece (Thing): '''Any playing piece for the table game.''' __classes = set([]) _shadows = True _tiny = None @classmethod def __setup__(cls): pass def __init__(self): super(Piece, self).__init__() if not self.__class__ in Piece.__classes: self.__class__.__setup__() Piece.__classes.add(self.__class__) self.height = 1 self.leaving = None self.dragging = None self.tangible = True def get_topmost(self): if not self.joins: return self for join in self.joins: if not getattr(join, 'tangible', True): continue return join.get_topmost() return self def get_image(self): '''Fetches the proper image to be rendered.''' return None def get_rect(self, context, image=None): if image is None: image = self.get_image() if image is None: return Rect(0,0,0,0) specs = context.explain(self) place = specs[2] rect = image.get_rect() rect.center = (int(place[0]+0.00001), int(place[1]+0.00001) - int(place[2]+0.00001)) return rect def hits(self, context, hither, yon): rect = self.get_rect(context) if rect.collidepoint(hither[0], hither[1]): return self return None def hidden(self): return False def paint(self, context): image = self.get_image() if not image: return rect = self.get_rect(context, image) context.media.blit(image, rect.topleft) def paint_coords(self, context): if not Piece._tiny: Piece._tiny = App.resource("FreeSans.ttf", 10) specs = context.explain(self) place = specs[2] text = "V(%g,%g,%g)" % (place[0], place[1], place[2]) image = self.get_image() rect = self.get_rect(context, image) image = Piece._tiny.render(text, 1, (0,0,0)) context.media.blit(image, rect.topleft) def drag(self, mouse): space = self.space() if not space or not space.can_drag(self): return if not self.is_style('draggable'): return if not self.leaving: self.leaving = self.frame self.follow(space) self.set_style('dragging') if not self.dragging: self.dragging = DragGoal(self) self.add_goal(self.dragging) def target(self, mouse): space = self.space() if hasattr(space, 'nearest_drop'): (what, where) = space.nearest_drop(self, mouse) else: (what, where) = (space, V(mouse[0], mouse[1], 0)) where[2] += self.height return (what, where) def drop(self, mouse): (what, where) = self.target(mouse) #print repr( (what, where) ) self.set_style('dragging', False) self.leaving = None self.dragging.drop() self.dragging = None self.goto(what, where) def goto(self, what, where=None): self.follow(what) self.wait() if where is None: where = V() self.move(where, dt=0.15) self.wait() def event(self, event): super(Piece, self).event(event) if event.type == MOUSEBUTTONDOWN: where = V(event.pos[0], event.pos[1]) if not self.is_style('dragging'): self.drag(where) if event.type == MOUSEMOTION: where = V(event.pos[0], event.pos[1]) if self.is_style('dragging'): self.drag(where) if event.type == MOUSEBUTTONUP: where = V(event.pos[0], event.pos[1]) if self.is_style('dragging'): self.drop(where) #---------------------------------------------------------------------------- class DragGoal (ChangeGoal): def __init__(self, thing): super(DragGoal, self).__init__(thing.place, [ None ], dt=0.25) self.done = False def progress(self): if self.done: return 1.0 mouse = pygame.mouse.get_pos() goal = V(self.variable) goal[0] = mouse[0] goal[1] = mouse[1] goal[2] = 20 self.controls = [ self.variable, self.variable, goal ] good = super(DragGoal, self).progress() return min(good, 0.999) def drop(self): self.done = True class BoingGoal (ChangeGoal): def __init__(self, thing, amount=1.0, dt=0.5): para = [ None, V(0,0,-0.25*amount), V(0,0,amount), V(0,0,amount), V(0,0,0) ] super(BoingGoal, self).__init__(thing.place, para, relative=True, dt=dt) class ShakeGoal (ChangeGoal): def __init__(self, thing, amount=1.0, cycles=1, dt=0.5): para = [ V(-amount,0,0), V(-amount,0,0), V(amount,0,0), V(amount,0,0) ] para = [ None ] + para*int(cycles) + [ V(0,0,0) ] super(QuakeGoal, self).__init__(thing.place, para, relative=True, dt=dt) #---------------------------------------------------------------------------- class Shadow (Piece): __images = { } @classmethod def __setup__(cls): for shape in [ 'card', 'pawn' ]: for level in [ 'hard', 'medium', 'soft' ]: name = "%s-shadow-%s" % (shape, level) Shadow.__images[name] = App.resource(name + '.png') def __init__(self, shape): super(Shadow, self).__init__() self.tangible = False self.shape = shape self.place = V(0, 0, -0.2) def hits(self, context, hither, yon): return None def get_level(self, place=None): frame = self.frame if not place: place = frame.xform.translation() place = place[2] place = int((place+0.0001) * 100.) / 100. if place <= 0.2: return None level = 'hard' if place > 3.0: level = 'medium' if place > 6.0: level = 'soft' return level def get_rect(self, context, image=None): #ours = super(Shadow, self).get_rect(context, image) # figure a new center based on their height place = self.frame.xform.translation() theirs = self.frame.get_rect(context) level = self.get_level(place) if level is None: return theirs level = { 'hard': 14, 'medium': 25, 'soft': 58 }[level] center = theirs.center theirs.width = theirs.width + level theirs.height = theirs.height + level theirs.center = (center[0], center[1] + place[2]*1.0) return theirs def get_image(self): level = self.get_level() name = "%s-shadow-%s" % (self.shape, level) if not name in Shadow.__images: return None image = Shadow.__images[name] return image def paint(self, context): if not self.frame: return if self.frame.hidden(): return image = self.get_image() if not image: return specs = context.explain(self) place = specs[2] frect = self.get_rect(context, image) irect = image.get_rect() image = pygame.transform.scale(image, (frect.width, frect.height)) context.media.blit(image, frect.topleft) class Sparkle (Piece): __images = { } @classmethod def __setup__(cls): image = App.resource('sparkles.png') for i in range(4): Sparkle.__images[i] = pygame.transform.rotozoom(image, 90*i, 3.0) def __init__(self, piece, dt=0.3): super(Sparkle, self).__init__() self.follow(piece) self.place = V(0, 0, +0.2) self.age = V(0) goal = ChangeGoal(self.age, V(1.0), dt=dt, todo=lambda g,s=self: s.unfollow()) self.add_goal(goal) self.wait() def hits(self, context, hither, yon): return None def get_image(self): image = random.choice(Sparkle.__images.values()) return image #---------------------------------------------------------------------------- class Pawn (Piece): '''A standard board game playing place-marker.''' __images = { } @classmethod def __setup__(cls): if Pawn.__images: return Pawn.__images.update( App.resources('pawn-hi-*.png') ) Pawn.__images.update( App.resources('pawn-iso-*.png') ) for name in Pawn.__images: image = Pawn.__images[name] image = pygame.transform.rotozoom(image, 0, 0.6) Pawn.__images[name] = image def __init__(self, name='green'): super(Pawn, self).__init__() self.name = name self.tilt = 'hi' self.height = 1. self.set_style('pickable') self.set_style('draggable') if Piece._shadows: shadow = Shadow('pawn') shadow.follow(self) shadow.move(V(0, 3, 0), relative=True) def get_image(self): name = "pawn-%s-%s.png" % (self.tilt, self.name) if not name in Pawn.__images: return None return Pawn.__images[name] def walk(self, targets): pass #---------------------------------------------------------------------------- class Die (Piece): '''A standard board-game playing die, usually a plain six-sider. Can support custom dice of almost any makeup, including loaded dice with uneven distribution of outcomes, or unusual geometry. ''' __images = { } @classmethod def __setup__(cls): if Die.__images: return Die.__images.update( App.resources('die-hi-*.png') ) Die.__images.update( App.resources('die-iso-*.png') ) for name in Die.__images: image = Die.__images[name] image = pygame.transform.rotozoom(image, 0, 0.8) Die.__images[name] = image def __init__(self, sides=6): '''Creates a playing die. With no arguments, makes a plain 1~6 fair die, like Yahtzee. Specify the number of sides for a plain 1~N fair die like AD&D. Specify a list of names for each side for a fair die with custom sides (like Cosmic Wimpout). Specify a dict with names associated with numerical weights for a loaded (unfair) die. The side names of a die are used to locate the image resources. ''' super(Die, self).__init__() self.height = 6 self.place = V(0,0,self.height) self.load = None if isinstance(sides, (list, tuple)): self.sides = list(sides[:]) elif isinstance(sides, dict): self.sides = sides.keys() self.load = sides else: self.sides = [ ('die-hi-%d.png' % (x+1)) for x in range(sides) ] self.side = 0 self.next = None self.set_style('pickable') self.set_style('draggable') if Piece._shadows: Shadow('pawn').follow(self) def hits(self, context, hither, yon): if not self.is_idle(): return None return super(Die, self).hits(context, hither, yon) def get_image(self): '''Fetches the proper image to be rendered.''' side = self.get() if side is None: # rework for non-stock dice image sets image = Die.__images[random.choice(Die.__images.keys())] else: image = Die.__images[side] return image def get(self): '''Returns the name of the visible side, or None if still rolling.''' if self.side is None: return None return self.sides[self.side] def pick(self): '''Returns a valid side of the die at random. If the die is unfair, the numerical weights are applied. This routine does not actually change the die; see settle(). ''' #TODO: honor self.load return random.choice(range(len(self.sides))) def settle(self): '''If the die is rolling, this commits to a winning side.''' if self.side is None: self.side = self.pick() def roll(self, dt=0.75): '''Begins the rolling process. Optionally specify the length of time to "roll" visually. While rolling, the side retrieved with get() is indeterminate. ''' if self.side is None: return self.side = None if dt > 0.0: self.move( [ None, (V.random(3) * M.scale(V(20,20,0))).order(3) + V(0,0,self.height + random.random()), (V.random(3) * M.scale(V(20,20,0))).order(3) + V(0,0,self.height + random.random()), (V.random(3) * M.scale(V(20,20,0))).order(3) + V(0,0,self.height + random.random()), V(0, 0, 0) ], relative=True, dt=dt) self.wait() self.append_goal(Goal(todo=lambda g,d=self: d.settle())) else: self.settle() def drop(self, where): super(Die, self).drop(where) if self.side is not None: self.roll() #---------------------------------------------------------------------------- class Card (Piece): __images = { } __font = None @classmethod def __setup__(cls): if Card.__font: return Card.__font = App.resource("FreeSans.ttf", 50) def __init__(self, sides, font=None, size=1.0): '''Creates a playing card gamepiece, with at least two sides. Each side is usually a string which identifies a resource. In the case of a missing resource, the string is rendered in black text centered on a blank card. ''' super(Card, self).__init__() if len(sides) < 2: raise ValueError, 'expect at least two sides on a card' self.sides = list(sides[:]) self.side = 0 self.next = 1 self.height = 0.01 self.place = V(0, 0, self.height) self.spin = V(0.0) self.font = font self.size = size self.set_style('pickable') self.set_style('flippable') self.set_style('draggable') if Piece._shadows: Shadow('card').follow(self) def make_image(self, side): key = (side,self.size) if key in Card.__images: return Card.__images[key] if App.find_resource(side + '.png'): image = App.resource(side + '.png') elif App.find_resource('card-%s.png' % side): image = App.resource('card-%s.png' % side) else: image = App.resource('card-blank.png').copy() center = image.get_rect().center font = self.font if not font: font = Card.__font text = font.render(side, 1, (0,0,0)) rect = text.get_rect() #TODO: shrink if text is too big rect.center = center image.blit(text, rect) image = image.convert_alpha() if self.size != 1.0: image = pygame.transform.rotozoom(image, 0, self.size) Card.__images[key] = image return image def hits(self, context, hither, yon): if not self.is_idle(): return None return super(Card, self).hits(context, hither, yon) def get_visible_side(self): if self.spin[0] < 0.5: return self.side return self.next def get_image(self): side = self.get_visible_side() return self.make_image(self.sides[side]) def advance(self): self.side = self.next self.next = (self.next + 1) % len(self.sides) self.spin[0] = 0.0 def flip(self, side=None, dt=0.5): sides = len(self.sides) if side is not None: self.next = side if dt > 0.0: self.move( [ None, V(-12, 0, 10), V( 0, 0, 15), V(+12, 0, 10), V.O ], relative=True, dt=dt ) goal = SerialGoal( [ TimeGoal(dt=dt/4.0), ChangeGoal(self.spin, V(1.0), dt=dt/2.0, todo=lambda g,card=self: card.advance()) ] ) self.add_goal(goal) self.wait() else: self.advance() return self.side def dazzle(self): sparks = Sparkle(self) self.flip(dt=0.1) self.flip(dt=0.1) self.flip(dt=0.1) def get_rect(self, context, image=None): rect = super(Card, self).get_rect(context, image) if self.spin[0] != 0.0: center = rect.center flop = math.cos(self.spin[0] * math.pi) t = flop * rect.width t = max(2, abs(t)) rect.width = t rect.center = center return rect def hidden(self): '''Weak optimization; we can avoid drawing if we know we're buried.''' pile = [ thing for thing in self.joins if isinstance(thing, Card) ] for other in pile: if other.place == V(0, 0, other.height): return True return False def paint(self, context): if self.hidden(): return specs = context.explain(self) place = specs[2] image = self.get_image() rect = self.get_rect(context, image) if self.spin[0] != 0.0: image = pygame.transform.scale(image, (rect.width, rect.height)) context.media.blit(image, rect.topleft) def drop(self, where): leaving = self.leaving (arriving, arrival) = self.target(where) if arriving is leaving: if self.is_style('flippable'): self.flip() super(Card, self).drop(where) def event(self, event): super(Card, self).event(event) if event.type == MOUSEBUTTONDOWN: if self.spin[0] != 0.0: return if event.type == MOUSEBUTTONUP: if self.spin[0] != 0.0: return class Ingot (Card): __images = { } __font = None @classmethod def __setup__(cls): if Ingot.__font: return Ingot.__font = App.resource("FreeSans.ttf", 100) def __init__(self, sides, font=None, size=0.75): '''Creates a flat thick gamepiece, with at least two sides. Stock graphics are included for common wooden letter tiles. Each side is usually a string which identifies a resource. In the case of a missing resource, the string is rendered in dark text centered on a blank card. ''' super(Ingot, self).__init__(sides, font=font, size=size) self.height = 0.1 self.place = V(0, 0, self.height) def make_image(self, side): key = (side, self.size) if key in Ingot.__images: return Ingot.__images[key] if App.find_resource(side + '.png'): image = App.resource(side + '.png') else: n = random.choice( range(6) ) image = App.resource('tile-%d.png' % n).copy() center = image.get_rect().center font = self.font if not font: font = Ingot.__font for offset in [ (-1,-1, (0x10,0x10,0x00)), (+1,+1, (0xC0,0x80,0x20)), ( 0, 0, (0x60,0x40,0x10)) ]: text = font.render(side, 1, offset[2]) rect = text.get_rect() #TODO: shrink if text is too big rect.center = (center[0]+offset[0], center[1]+offset[1]) image.blit(text, rect) image = image.convert_alpha() if self.size != 1.0: image = pygame.transform.rotozoom(image, 0, self.size) Ingot.__images[key] = image return image def get_rect(self, context, image=None): rect = super(Ingot, self).get_rect(context, image) if self.spin[0] != 0.0: center = rect.center flop = math.cos(self.spin[0] * math.pi) t = flop * rect.width t = max(8, abs(t)) rect.width = t rect.center = center return rect #---------------------------------------------------------------------------- class CardDeck (Piece): # if we admit a card, push it onto us # if user tries to drag us, pop our head and drag that # if we pop to nothing, we die # paint a stack, then our head pass #---------------------------------------------------------------------------- class ClingPad (Piece): '''A toy drawing pad, with a thin translucent sheet over a wax board. The user can sketch on the sheet and make it cling to the wax. Dragging the corner of the cel sheet "erases" the drawing as it breaks free from the clinging wax. ''' def __init__(self, size=None, guide=None): super(ClingPad, self).__init__() self.wax = App.resource('clingpad-wax.png') self.cel = App.resource('clingpad-cel.png') self.tug = App.resource('clingpad-tug.png') self.nib = App.resource('spot-12x12.png') self.rib = Rect(37,0,342,33) self.map = Rect(37,33,342,339) self.rip = Rect(37,33+339-30,50,30) self.fat = self.nib.get_rect() self.fat = (self.fat.width/2,self.fat.height/2) self.pad = App.image(self.map.size, SRCALPHA) self.figure = Figure(ordered=True) self.stylus = V(0, 0) self.press = False self.tugging = V(0) self.erasing = V(0) self.set_style('pickable') self.set_style('flippable') self.set_style('draggable') self.set_guide(guide) # if Piece._shadows: Shadow('card').follow(self) def stop(self): super(ClingPad, self).stop() self.tugging = V(0) self.erasing = V(0) self.press = False self.figure.stop() def set_guide(self, guide): if guide: map = Rect(self.map) ; map.inflate(-20, -20) rect = guide.get_rect() rect = rect.fit(map) self.guide = pygame.transform.scale(guide, rect.size) else: self.guide = None def draw(self, where): d = distance(self.stylus, where) for t in range(0, int(d), self.fat[0]): stylus = interpolations.linear(0, d, t, self.stylus, where) self.pad.blit(self.nib, (stylus[0]-self.fat[0], stylus[1]-self.fat[1])) self.stylus = where def hits(self, context, hither, yon): h = super(ClingPad, self).hits(context, hither, yon) if h: return h wax = self.wax.get_rect() wax.center = (self.place[0], self.place[1]) map = pygame.Rect(self.map) map.left += wax.left map.height += self.rib.height if not map.collidepoint( (hither[0],hither[1]) ): return None return self def curl(self, amount): self.tugging = V(min(200, max(26, amount))) self.erasing = V(max(26, int((amount-13)*2))) erase = pygame.Rect(self.map) erase.left = 0 erase.top = erase.height - self.erasing[0] self.pad.fill( (0,0,0,0), erase ) def uncurl(self): self.add_goal(ChangeGoal(self.tugging, V(0), dt=0.15*self.tugging[0]/200.)) self.add_goal(ChangeGoal(self.erasing, V(0), dt=0.25*self.tugging[0]/200.)) self.figure = Figure(ordered=True) def clear(self): self.wait() self.add_goal(ChangeGoal(self.tugging, V(200), dt=0.35)) self.add_goal(ChangeGoal(self.erasing, V(500), dt=0.25)) self.wait(todo=lambda g,s=self: s.pad.fill( (0,0,0,0) )) self.wait(todo=lambda g,s=self: s.uncurl()) def is_idle(self): if self.tugging: return False if self.erasing: return False if self.press: return False return super(ClingPad, self).is_idle() def event(self, event): #super(ClingPad, self).event(event) size = self.wax.get_rect().size if event.type in (MOUSEBUTTONDOWN,MOUSEMOTION,MOUSEBUTTONUP): where = V(event.pos[0]-self.place[0]+size[0]/2, event.pos[1]-self.place[1]+size[1]/2) if event.type == MOUSEBUTTONDOWN: if not self.tugging: if self.rib.collidepoint(where): if not self.dragging and self.is_style('draggable'): self.drag(V(event.pos[0], event.pos[1])) elif self.rip.collidepoint(where): self.curl(self.map.bottom-where[1]) self.press = False elif self.map.collidepoint(where): self.press = True where -= V(self.map.left, self.map.top) self.stylus = where + V(0,1) self.figure.start(where) self.draw(where) if event.type == MOUSEMOTION: if self.dragging: self.drag(V(event.pos[0], event.pos[1])) elif self.tugging: self.curl(self.map.bottom-where[1]) elif self.press: if not self.map.collidepoint(where): self.press = False elif self.rip.collidepoint(where): self.press = False else: where -= V(self.map.left, self.map.top) self.figure.draw(where) self.draw(where) if event.type == MOUSEBUTTONUP: if self.dragging: self.drop(V(event.pos[0], event.pos[1])) if self.tugging: self.stop() self.uncurl() if self.press: self.press = False if self.map.collidepoint(where): where -= V(self.map.left, self.map.top) self.draw(where) self.figure.stop() self.figure.cook(jitter=20) def paint(self, context): wax = self.wax.get_rect() wax.center = (self.place[0], self.place[1]) where = wax.topleft context.media.blit(self.wax, where) inset = (where[0] + self.map.left, where[1] + self.map.top) if self.tugging or self.erasing: # amount of curled/uncurled cello tugging = max(26, self.tugging[0]) tugging = wax.top + self.map.bottom - tugging # uncurled cello area = self.cel.get_rect() area.height = tugging - wax.top cel = self.cel.subsurface(area) context.media.blit(cel, where) area.height = self.cel.get_rect().height - self.erasing[0] - 10 #cel = self.cel.subsurface(area) if area.height > 0: context.media.blit(self.cel, where, area) # clingy artwork area = self.pad.get_rect() area.height = self.pad.get_rect().height - self.erasing[0] if area.height > 0: context.media.blit(self.pad, inset, area) # curled up cello if self.erasing[0] >= 26: tug = list(self.tug.get_rect().size) tug[1] = int(interpolations.linear(26, 200, max(26, self.tugging[0]), 8, tug[1])) image = pygame.transform.scale(self.tug, tug) context.media.blit(image, (wax.left, tugging-tug[1]/2)) else: context.media.blit(self.cel, where) context.media.blit(self.cel, where) if self.guide: rect = self.guide.get_rect() rect.center = (inset[0]+self.map.width/2, inset[1]+self.map.height/2) context.media.blit(self.guide, rect) context.media.blit(self.pad, inset) #---------------------------------------------------------------------------- import Numeric import pygame.surfarray as surfarray class _TestSpace (Space): def __init__(self, *args): super(_TestSpace, self).__init__(*args) self.text = [ "Draw on the cling pad.", "Press Esc to quit." ] self.font = App.resource("FreeSans.ttf", 25) self.pad = ClingPad( ) self.pad.follow(self) self.pad.move( V(450,210,0) ) font = App.resource("FreeSans.ttf", 300) guide = font.render("G", 1, (0x00,0x40,0x00)) sa = surfarray.pixels_alpha(guide) sa /= Numeric.array(8).astype(Numeric.UInt8) self.pad.set_guide(guide) self.ingot = Ingot( ('A','B') ) self.ingot.follow(self) self.ingot.move( V(100,310,0) ) self.quit = False def can_drag(self, thing): return True def segue(self): if self.quit: return None return super(_TestSpace, self).segue() def event(self, event): if event.type == KEYDOWN: if event.key == K_SPACE: self.pad.stop() self.pad.clear() if event.key == K_ESCAPE: self.quit = True while len(self.text) > 10: self.text.pop(0) super(_TestSpace, self).event(event) def paint(self, surface): super(_TestSpace, self).paint(surface) y = 25 for line in self.text: text = self.font.render( line, 1, (0xFF,0xCC,0x66) ) surface.blit( text, (15, y) ) y += text.get_rect().height if __name__ == '__main__': # see "boards.py" for a meaningful demonstration using these pieces app = App('Demo - press Esc to quit') app.space = _TestSpace() app.run() |
|
Contact Ed Halley by email at
ed@halley.cc. Text, code, layout and artwork are Copyright © 1996-2013 Ed Halley. Copying in whole or in part, with author attribution, is expressly allowed. Any references to trademarks are illustrative and are controlled by their respective owners. |
|