Glyph

Glyph is a small Python library that simplifies text-based GUI's and games. Its fundamental class, the Glyph (surprise!) consists of some number of ASCII images and a few other attributes (including position and layer). Glyphs relate to each other in a parent-child context. When a glyph is rendered, it renders each of its children, at the position they specify. The glyph inserts itself under any children with a positive layer number.

Each Glyph saves its image in a dictionary, where keys are [x, y] positions and values are the characters in those locales. When rendered, each Glyph's image is composited with all its children, then converted into a string for printing.

It doesn't sound so useful yet, does it? I'll present a few examples of its utility.

  • Simple games. The background acts as the root parent, and its children include the character and any interactive elements.
  • Status screens. Glyph simplifies building even a complicated status screen, full of both text and ASCII images. It's easy to place text within a frame, or organize data in rows and columns. If each piece of data is a Glyph, then they can be updated independently with the setImage function.

Shortcomings:

  • Because all positions are relative to the parent, but not the root, Glyph becomes confusing if the children have children. Future versions will have parents to track children, and so allow absolute positions
  • Glyph has no easy way to grab characters or user input. MS has the getch library, but LINUX doesn't have a parallel. There is a code snippet online (reproduced below) that allows grabbing characters without echoing them to the screen, but it hangs until a character is recieved.

glyph.py

#!/usr/bin/python

#Benjamin Nitkin
#Summer 2012

#Framework for drawing things to a terminal, ala asciiquarium.
#The framework involves recursive rendering of "glyphs".

#This is the second version of Glyph. It does away with the child
#class, and relies on attributes of the Glyph itself. Some of these behaviors
#could be coanfusing. For instance, glyphs are composited in order of layer, but the
#parent glyph is ALWAYS layer zero. The layer that the user set applies when the
#glyph is read as a child, but not when it is treated as a parent.

#Intentional issues:
# - Any glyphs above the top left of the root parent will not render
#
#Known bugs:
# - all positions are relative to the first parent, so the coordinate
#   system quickly loses coherency
#
#
#
#Unknown bugs:
# - None
#
# ;)



class Glyph():
    """Generalities and Generic Arguments
text:  a string, with newlines, that the glyph will convert to an ascii image.
name:  a textual for an image (a glyph can have multiple images to
enable animation.)
image:    a nested dictionary. The first layer contains image names as keys,
and images as values. The images are themselves dictionaries, where the
keys are [x, y] coordinates and the values are characters.
offset:     offset, in characters, between the top left of the parent and
the corner of the glyph
children: a list of child glyphs.
child:  a single glyph, treated as a child (they sometimes pout)
transparency:  character(s) that will be rendered as transparent
(default: whitespace)

When setting attributes, use functions. When retrieving them,
use fields. For instance:

>>> glyph.setPos(5, 8)
>>> glyph.x
5
>>> glyph.height
4
"""


###Miscellanous Functions###
    def __init__(self, text, name = 'Default', layer = 0, transparency = '     ', x = 0, y = 0):
        """Initializes a glyph.
        text:  a string, with newlines, that the glyph will print
        name:  a name for the text imported.
        layer:  the layer of the glyph. A parent glyph behaves as though it
        lies on layer 0
        transparency:  A string of characters, any of which will render
        transparent.
        x:  The distance, in rows, between the left side of this glyph
        and its parent.
        y:  The distance, in columns, between the top of this glyph and
        its parent."""
        self.children = []
        self.parents = [] #Yes, a glyph can have multiple parents. No, I have no idea why you would use them.
        self.image = {}
        self.active = 'This will be the current image. Not yet set.'
        self.addImage(text, name, transparency) #Parse text into the image format
        self.setImage(name)
        self.layer = layer
        self.x = x
        self.y = y
        self.dirty = True #Whether this, and its children, need to be rerendered
        
        #Calculated by setImage()
        #self.height = ???
        #self.width= ???
        

    def __repr__(self):
        """Prints vital stats about a glyph: size, offset, and layer."""
        return 'Glyph. dimensions: {w}x{h}, offset: {x}x{y}, layer {layer}'.format(w=self.width, h=self.height, x=self.x, y=self.y, layer=self.layer)

    
    def __lt__(self, other):
        """le must be defined for cmp() to work, which list.sort() uses.
        This implementation sorts Glyphs by their layer."""
        if self.layer < other.layer: return True
        else: return False
    
    
###Rendering Functions###
    def __str__(self):
        """Returns a string of the glyph, suitiable for printing.
        Will eventually implement character positioning characters,
        which might make this linux-only."""
        
        #1. Recursively build a map of all characters & their positions
        complete = self.composite({})
        # I shouldn't need to specify {} as the initial image, but using
        # image={} as an argument in composite() doesn't work right:
        # Python 'caches' old images, and they become the background.
        

        #2. Find dimensions of the final glyph
        maxi = [0, 0]
        for x, y in complete:
            if x > maxi[0]: maxi[0] = x
            if y > maxi[1]: maxi[1] = y

        # Add one to accomodate for the 0th index
        maxi[0]+=1
        maxi[1]+=1
        
        #3. Create a text map, using strings for rows, and a list to hold the
        #columns.
        rendered = ((' '*maxi[0]+'\n')*maxi[1])
        #Convert to a list for easier handling. This way,
        #the strings aren't shallow copies of each other.
        #Cause that's annoying.
        rendered = rendered.split('\n')
        
        #4. Blit data to the text map
        for x, y in complete:
            #Handling for out-of-bounds characters.
            if (x > maxi[0]) or (y > maxi[1]): continue
            if (x < 0) or (y < 0): continue
            #Modify the correct row
            rendered[y] = rendered[y][:x]+complete[x, y]+rendered[y][x+1:]
        return '\n'.join(rendered)
            
            
    def clean(self):
        """Check if this glyph, with its current children, needs to be rerendered.
        returns True or False"""
        if self.dirty: return False
        for child in self.children: #Check children recursively
            if child.clean(): return False
        return True #Nothing dirty.
        
    
    def composite(self, image, dx = 0, dy = 0):
        """Recursively composits glyphs onto each other.
        That is, this function takes a parent glyph and iterates through
        child glyphs to form a comprehensive image map, using the
        dictionary format described above. __str__() transforms
        the return value of this into a string. Another function will
        output positioning characters, instead, to reduce output."""
        
        #If the image is clean, don't bother.
        if self.clean() == True: return self.imageCache
        self.dirty = False
        
        #Sort children by layer
        self.children.sort()
        needToBlitSelf = True
        
        for child in self.children:
            if (child.layer > 0) and needToBlitSelf:
                needToBlitSelf = False
                for x, y in self.active:
                    image[x+dx, y+dy] = self.active[x, y]
            #Cascade - recurse - depthify - whatever
            image = child.composite(image, dx+child.x, dy+child.y)
            
        if needToBlitSelf:
            #No children had a positive layer, so we need to blit this glyph.
            for x, y in self.active:
                image[x+dx, y+dy] = self.active[x, y]
                
        self.imageCache = image
        return image
    
    def copy(self):
        """Returns a shallow copy of the glyph."""
        clone = Glyph('Dummy text')
        clone.__dict__ = self.__dict__.copy()
        return clone
        
        
###Offset and Layer Functions###
    def setPos(self, x = None, y = None):
        """Sets the x and y offset between a glyph and its parent.
        Preserves current position if argument is unspecified."""
        if x != None or y != None: self.dirty = True
        if x != None: self.x = x
        if y != None: self.y = y
    
    def setAbsPos(self, x = 0, y = 0):
        self.dirty = True
        
        
    def move(self, dx=0, dy=0):
        """Moves a glyph, relative to its parent
        dx:    Columns to move left
        dy: Rows to move down"""
        self.dirty = True
        self.x+=dx
        self.y+=dy
    
    def setLayer(self, layer):
        """Sets the glyphs layer. Negative numbers are rendered first.
        The parent is rendered on layer zero, and positive layers lie above."""
        self.dirty = True
        self.layer = layer
        
        
###Image Functions###
    def addImageFromFile(self, fileName, name, transparency = ''):
        """Reads the specified file, and imports its full text as a glyph.
        fileName:        The path to the file.
        name:            A name for the new image
        transparency:    Characters that will appear transparent in the image."""
        addImage(file(fileName, 'r').read(), name, transparency)
        

    def addImage(self, text, name, transparency = ''):
        """Glyphs are permitted to have multiple images. Think of them as
        sprites, which simplify animation and states. This function loads
        new images into the glyph, from a 'raw' text format
        (ACSII art, shaped by characters and newlines).
        text:    The art being imported.
        name:    The name that will be assigned to the new image
        transparency: The characters that will appear transparent in the image"""        
        self.image[name] = {}
        rows = text.split('\n')
        for y, row in enumerate(rows):
            for x, col in enumerate(row):
                if col in transparency: pass
                else: self.image[name][x, y] = col
    
        #If that image was active, its dirty now.
        if self.image[name] == self.active: self.dirty = True
    
    def setImage(self, name):
        """A glyph can have any number of images stored in it, but only
        the active one is visible. setImage selects the active image.
        name:    The name of the image to display. The first image in
        the glyph is named "Default", and others are named by the user."""

        if self.image[name] != self.active: self.dirty = True
        self.active = self.image[name]
        
        #Find dimensions of new image
        self.height, self.width = (0, 0)
        for x, y in self.active.keys():
            if x>self.width:  self.width  = x
            if y>self.height: self.height = y
            
            
    def deleteImage(self, name):
        """deleteImage removes images from memory.
        Useful if images are becoming unmanagable.
        name:    The name of the image to delete"""
        if self.active == self.image[name]:
            raise Exception, "The active image cannot be deleted"        
        else:
            del self.image[name]            
            
            
###Children Functions###
    def addChild(self, glyph, layer = None, x = None, y = None):
        """addChild associates two glyphs in a parent-child relationship.
        glyph:    The new child glyph
        layer:     The layer to place the child on. Layer is preserved when unspecified.
        x and y:    The offsets to use, between the top left
        corner of the parent and child. Preserved if unspecified."""
        #Modify selected characteristics
        if layer != None:             glyph.setLayer(layer)
        if x!=None and y!=None:        glyph.setOffset(x, y)
        self.children.append(glyph)
        self.dirty = True
        
        
    def setChildren(self, children):
        """Completely replaces the children of a glyph with new ones.
        children: a list of child glyphs"""
        #Replaces the current children
        if (type(children) == type(['list'])):
            self.children = children
            self.dirty = True
        else:
            raise TypeError, "children must be a list"
    
    def deleteChild(self, glyph):
        """Removes a child glyph from a parent.
        glyph:    The child to remove"""
        self.children.remove(glyph)
        self.dirty = True
    
    def getChildren(self):
        """Returns a nested dictionary of this glyphs children"""
        listing = {}
        for child in self.children:
            listing[child] = child.getChildren()
        return listing
        
    def getParents(self):
        """Returns a nested dictionary of this glyphs parents"""
    
    def prettifyDict(self, dictionary, head = ''):
        """Prints the nested dictionary that getParents and
        getChildren generate in a nice format."""
        for key in dictionary.keys():
            print head, key.__repr__()
            self.prettifyDict(dictionary[key], head + '| ')
            

while 1:
    import sys
    print 1
    print sys.stdin.read(1)

def test():
    """A 10 second demo of some abilities of Glyph. The second portion
    is animation, and buries the first few demos.
    Piping output through less may be helpful."""
    import time, random
    #A quick test to run Glyph through its paces.
    a = """______
|    |
|    |
|    |
|    |
|____|"""

    b = """/\\
\\/"""
    c = """ O
/|\\
 |
/ \\"""


    sm = """...
...
..."""
    med = """      aaa
   aaaaaaaaa
 aaaaaaaaaaaaa
aaaaaaaaaaaaaaa
 aaaaaaaaaaaaa
   aaaaaaaaa
      aaa"""
    lg = """___________________________________
|                                 |
|                                 |
|                                 |
|                                 |
|                                 |
|                                 |
|                                 |
|                                 |
|                                 |
|                                 |
|                                 |
|                                 |
|                                 |
|_________________________________|"""

    one = Glyph(a)
    two = Glyph(b)
    three = Glyph(c)
    lg = Glyph(lg)
    med = Glyph(med)
    sm = Glyph(sm)
    lg.addChild(med)
    med.addChild(sm)
    med.addChild(two)
    lg.addChild(one)
    x= lg.getChildren()
    print type(x)
    print lg.prettifyDict(x)
test()

platform.py - a short demo of Glyph. Currently incomplete.

#!/usr/bin/python
#Simple platform game to demo glyph
import time, random
import getchar
from glyph import Glyph
class Character():
    def __init__(self, x, y):
        self.y = y
        self.x = x
        
        self.cycle = [0,1] #Cycle of images
        self.stage = 0
        
        self.jumpStage = 0
        self.dieStage = 0
        
        self.character = Glyph('   O>\n  /\\\n /  \\\n  \'\'', name=0)
        self.character.addImage('   O>\n  /\\\n /  \\\n  \"\"', name=1)
        self.character.setPos(x,y)
        
    def die(self):
        #Fall off of the screen
        self.dieStage += 1
        height = int(3.0/49*(self.dieStage)**2)
        self.setPos(y=self.y+height)

    def startDie(self):
        if self.dieStage == 0: self.dieStage = 1
        
    def step(self):
        #Pick the next image in sequence
        self.stage += 1
        self.stage = self.stage % len(self.cycle)
        self.character.setImage(self.cycle[self.stage])
        
        if self.jumpStage != 0: self.jump()
        
    def jump(self):
        #Jump over ~15 steps
        if self.jumpStage == 15:
            self.jumpStage = 0
            return
        else: self.jumpStage += 1
        height = 3-int(3.0/49*(self.jumpStage-7)**2)
        self.character.setPos(self.x, self.y-height)

    def jumpStart(self):
        if self.jumpStage ==0: self.jumpStage = 1
        
class Floor():
    def __init__(self, y, char, width):
        self.y = y
        self.floor = Glyph('')
        self.tile = Glyph(char)
        self.tree = Glyph('  ^^^^\n ^^^^^^\n  ^^^^\n   []\n   []/\n   []')
        self.width = width
        
        #Whether to lay a floor, and time til switching
        self.current = True
        self.switch = 35
        
    def step(self, floor = None):
        #decide whether to lay new floor
        #Deltas to transition from the floors coordinates to a global system
        dx = self.floor.x*-1
        dy = self.floor.y*-1
        
        self.switch-=1
        
        if self.switch == 0:
            self.current = not self.current
            if self.current:
                self.switch = random.randint(5, 50)
            else:
                self.switch = 10


        if self.current:
            #Add a block to the floor
            self.floor.addChild(self.tile.copy(), layer=-1, x=self.width+dx, y=self.y+dy)
            if random.random() > 0.99:
                self.lastTree = 0
                self.floor.addChild(self.tree.copy(), layer = 2, x = self.width+dx, y = self.y-self.tree.height+dy-1)
        
        #Move and cleanup
        self.floor.move(dx=-1)
        for child in self.floor.children:
            if self.floor.x+child.x<0: del child
        
def main():
    parent = Glyph("""____________________________________________________________________________          
|                                                                          |          
|                                                                          |          
|                                                                          |          
|                                                                          |          
|                                                                          |          
|                                                                          |          
|                                                                          |          
|                                                                          |          
|                                                                          |          
|__________________________________________________________________________|          """)
    dude = Character(2, parent.height-5)
    parent.addChild(dude.character, layer = 1)
    
    floor = Floor(parent.height-1, 'T', parent.width-2)
    parent.addChild(floor.floor, layer = -1, x=1)
    
    keyboard = Input()
    while 1:
    
        floor.step()
        dude.step()
        
        x = floor.floor.x
        tiles = floor.floor.children
        
        die = False
        for tile in tiles:
            if x-4 == -1*tile.x:
                die = True
        if die and floor.x < floor.width: dude.dieStart()
        if ' ' in keyboard.timed(0.1): dude.jumpStart
        print parent
main()

getchar.py - an attempt at a getch style input. The _Getch() function hangs until a character is recieved, so I'm working to implement a timeout.

#!/usr/bin/python
#Additions by Benjamin Nitkin
#Summer 2011

#_Getch code from
#http://code.activestate.com/recipes/134892-getch-like-unbuffered-character-reading-from-stdin/

import time

class _Getch:
    """Gets a single character from standard input.  Does not echo to the
screen."""
    def __init__(self):
        try:
            self.impl = _GetchWindows()
        except ImportError:
            self.impl = _GetchUnix()

    def __call__(self): return self.impl()


class _GetchUnix:
    def __init__(self):
        import tty, sys

    def __call__(self):
        import sys, tty, termios
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(sys.stdin.fileno())
            ch = sys.stdin.read(1)
            if ch == '': raise KeyboardInterrupt, 'getch breaks Ctrl+C, so it\'s reimplimented here'
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return ch


class _GetchWindows:
    def __init__(self):
        import msvcrt

    def __call__(self):
        import msvcrt
        return msvcrt.getch()

        
class Input:
    def __init__(self):
        from threading import Thread
        import time
        self.text = []
        self.run = False
        handle = Thread(target = self.thread)
        handle.start()
        
    def thread(self):
        getch= _Getch()
        while True:
            while self.run:
                x = getch()
                self.text.append(x)
            else:
                time.sleep(0.1)
    
    def clear(self):
        """Clear text buffer"""
        self.text = []
    
    def timed(self, timeout = 1):
        """Return characters entered before end condition:
        timeout:    Amount of time to gather, in seconds"""
        self.clear()
        self.run = True
        time.sleep(timeout)
        self.run = False
        return ''.join(self.text)
        
    def fixed(self, characters = 1):
        """Return characters entered before end condition:
        characters:    Number of characters to gather"""
        self.clear()
        self.run = True
        while len(self.text) <characters: time.sleep(0.01)
        self.run = False
        return self.text[:characters]