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
    caches.py
    cards.py
    constraints.py
    csql.py
    english.py
    getch.py
    getopts.py
    gizmos.py
    goals.py
    improv.py
    interpolations.py
    namespaces.py
    nihongo.py
    nodes.py
    octalplus.py
    patterns.py
    physics.py
    pids.py
    pieces.py
    quizzes.py
    recipes.py
    relays.py
    romaji.py
    ropen.py
    sheets.py
    stores.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.
Make donations with PayPal - it's fast, free and secure!