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]