By Ben Nitkin on
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]