Modularize.
authorNeil Moore <neil@s-z.org>
Tue, 27 May 2008 09:05:27 +0000 (05:05 -0400)
committerNeil Moore <neil@s-z.org>
Tue, 27 May 2008 19:24:41 +0000 (15:24 -0400)
Split code up into modules:
  level:    Level, LevelProxy, and LocationProxy
  loc:      Location and descendants.
  things:   Thing and descendants, Material, ItemClass
  ui:       Color
  __main__: AppUI
This division is not final.

Remove unused imports.

Eliminate most from...import statements.

level.py [new file with mode: 0644]
loc.py [new file with mode: 0644]
roguelike.py
things.py [new file with mode: 0644]
ui.py [new file with mode: 0644]

diff --git a/level.py b/level.py
new file mode 100644 (file)
index 0000000..045604f
--- /dev/null
+++ b/level.py
@@ -0,0 +1,341 @@
+import curses
+import random
+import math
+import re
+
+import loc
+import things
+import cacher
+import __main__
+
+class LocationProxy (object):
+    def __init__(self, level=None, y=None, x=None, locstr=None):
+        if locstr:
+            (lstr, ystr, xstr) = locstr.split(None, 2)
+            level = LevelProxy(lstr)
+            y = int(ystr)
+            x = int(xstr)
+        assert (level and y is not None and x is not None)
+        self.lvl = level
+        self.y = y
+        self.x = x
+
+    def level(self):
+        if isinstance(self.lvl, LevelProxy):
+            self.lvl = self.lvl.level()
+        return self.lvl
+
+    def location(self):
+        return self.level().loc(self.y, self.x)
+
+
+class Level (object):
+    """Represents an area of the map comprising a contiguous rectangular
+    region of Tiles surrounded by an impassable boundary.  Levels may
+    be travelled between via transport points.
+    """
+    __metaclass__ = cacher.FirstArg
+
+    def stair_unavailable(self, stair):
+        if stair in self.unconnected_stairs:
+            self.unconnected_stairs.remove(stair)
+    def stair_available(self, stair):
+        self.unconnected_stairs.add(stair)
+
+    def __init__(self, filename=None, height=None, width=None, name=None):
+        self.active_tiles = set()
+        self.invalid_tiles = set()
+        self.important_repaints = set()
+        self.unconnected_stairs = set()
+        self.unconnected_drops = []
+        self.needs_full_repaint = True
+        self.filename = None
+
+        self.pick_tile = random.choice((self.pick_tile_1, self.pick_tile))
+
+        if filename:
+            assert height == width == name == None, \
+                    "Level constructor specified more than just the filename"
+            self.load_file(open(filename, "r"))
+        else:
+            assert height and width
+            self.h = height
+            self.w = width
+            self.name = name or ""
+
+            self.grid = [ [ self.activate(self.pick_tile(i,j))
+                for j in xrange(self.w) ]
+                for i in xrange(self.h) ]
+
+        random.shuffle(self.unconnected_drops)
+
+        self.boardwin = curses.newwin(self.h + 1, self.w + 1)
+    
+    def activate(self, tile):
+        (y, x) = (tile.y, tile.x)
+        if tile.is_active():
+            self.active_tiles.add((y, x))
+        if isinstance(tile, loc.Stair) and not tile.target() and not tile.oneway:
+            self.stair_available(tile)
+        return tile
+
+    def load_file(self, f):
+        attrs = {}
+        def parse_line(line, lno):
+            linere = r'^(\d+)\s+(\d+)\s+(\w+)\s+(\w+)\s+(.*?)\s*\n$'
+            m = re.match(linere, line)
+            (y, x, attr, typ, valstr) = m.groups()
+            y = int(y) ; x = int(x)
+            if not 0 <= y < self.h:
+                raise Exception("extra out of bounds: y=%d" % (y))
+            if not 0 <= x < self.w:
+                raise Exception("extra out of bounds: x=%d" % (x))
+
+            val = None
+            if typ == "float":
+                val = float(valstr)
+            elif typ == "int":
+                val = int(valstr)
+            elif typ == "bool":
+                val = valstr != "False"
+            elif typ == "str":
+                val = valstr.strip()
+            elif typ == "level":
+                val = LevelProxy(valstr.strip())
+            elif typ == "loc":
+                val = LocationProxy(locstr = valstr.strip())
+            else:
+                raise Exception("unknown type %s (val '%s')" % (typ, valstr))
+            if (y,x) not in attrs:
+                attrs[(y,x)] = {}
+            attrs[(y,x)][attr] = val
+
+        line = f.readline() ; lno = 1
+        if line.rstrip('\n') != "#!RgLkLvL":
+            raise Exception("Bad first line: '%s'" % (line))
+
+        line = f.readline() ; lno = 2
+
+        (hs, ws, name) = re.split("\\s+", line, 2)
+        self.h = int(hs)
+        self.w = int(ws)
+        self.name = name.rstrip('\n')
+
+        self.grid = [[None] * self.w for x in xrange(self.h)]
+
+        lines = []
+
+        first_lno = lno + 1
+        for i in range(self.h):
+            lines.append(f.readline())
+            lno = lno + 1
+
+        for l in f.readlines():
+            lno = lno + 1
+            parse_line(l, lno)
+
+        for i in range(self.h):
+            line = lines[i]
+            if len(line) != self.w + 1:
+                raise Exception("incorrect line length: line %d = %d"
+                        % (first_lno + i, len(line)))
+            for j in range(self.w):
+                t = self.decode_tile(i, j, line[j],
+                        attrs[(i,j)] if (i,j) in attrs else {})
+                self.grid[i][j] = self.activate(t)
+            if line[self.w] != '\n':
+                raise Exception("line %d has no newline" % (i))
+
+
+        f.close()
+
+    def pick_tile_1(self, i, j):
+        if i == 15 and j == 60:
+            return loc.Stair(self, i, j)
+        elif (i-15)**2 + ((j-60)**2)/2 <= 36:
+            flowy = (j-60) / (math.sqrt(2) * 6) - (i-15)/(math.sqrt(2)*18)
+            flowx = (15-i) * math.sqrt(2) / 6 - (j-60)*math.sqrt(2)/18
+            return loc.Water(self, i, j, flowy, flowx)
+        elif 580 < (i-5)**2 + ((j-45)**2)/2 < 900:
+            flowy = (45-j) / (math.sqrt(2) * 27)
+            flowx = (i-5) * math.sqrt(2) / 27
+            return loc.Water(self, i, j, flowy, flowx)
+        elif random.random() < .1:
+            return loc.Wall(self, i, j)
+        else:
+            return loc.Floor(self, i, j, self.randitems())
+    
+    def pick_tile(self, i, j):
+        if i==0 or j==0 or i == self.h - 1 or j == self.w - 1:
+            return loc.Wall(self, i, j)
+        if (i == 5 and j == 9) or (i == self.h-5 and j == self.w - 10):
+            return loc.Stair(self, i, j)
+        if i % 9 == 0:
+            if (j == 9 and i % 18 == 0) or (j == self.w-10 and i % 18 == 9):
+                return loc.Door(self, i, j)
+            else:
+                return loc.Wall(self, i, j)
+        if j % 18 == 0:
+            if i % 9 == 4:
+                return loc.Door(self, i, j)
+            else:
+                return loc.Wall(self, i, j)
+        else:
+            return loc.Floor(self, i, j, self.randitems())
+
+    def get_target(self, oneway):
+        if oneway:
+            return self.unconnected_drops.pop()
+        else:
+            return self.unconnected_stairs.pop()
+
+    def invalidate(self, tile=None, important=False):
+        if tile == None:
+            self.needs_full_repaint = True
+        else:
+            assert tile.level() == self
+            self.invalid_tiles.add((tile.y, tile.x))
+            if important:
+                self.important_repaints.add((tile.y, tile.x))
+
+    def decode_tile(self, i, j, char, attrs):
+        if char == "#":
+            return loc.Wall(self, i, j, **attrs)
+        elif char == " ":
+            return loc.Floor(self, i, j, self.randitems(), **attrs)
+        elif char == "~":
+            return loc.Water(self, i, j, **attrs)
+        elif char == "+":
+            return loc.Door(self, i, j, opened = False, **attrs)
+        elif char == "-":
+            return loc.Door(self, i, j, opened = True, **attrs)
+        elif char == ">":
+            return loc.Stair(self, i, j, **attrs)
+        elif char == "<":
+            # Target for one-way stairs
+            tile = loc.Floor(self, i, j, **attrs)
+            self.unconnected_drops.append(tile)
+            return tile
+        else:
+            raise Exception
+
+    def randitems(self):
+        r = 1000*random.random()
+        if r >= 10:
+            return []
+        elif r >= 5:
+            return [ things.ItemClass("gold piece")() ]
+        elif r >= 2:
+            return [ things.ItemClass("gem")() ]
+        elif r >= 1:
+            return [ things.ItemClass("shield")() ]
+        else:
+            return [ things.ItemClass("sword")() ]
+
+    def loc(self, i, j):
+        return self.grid[i][j]
+
+    def clip(self, y, x):
+        if y<0:
+            y = 0
+        elif y >= self.h:
+            y = self.h-1
+        if x<0:
+            x = 0
+        elif x >= self.w:
+            x = self.w-1
+        return (y, x)
+
+    def players(self):
+        return (pl for pl in __main__.AppUI.instance.players if pl.level() == self)
+
+    def visible_path(self, py, px, oy, ox):
+        (dy, dx) = (oy-py, ox-px)
+        if abs(dy) <= 1 and abs(dx) <= 1:
+            return True
+        flipped = False
+        # Go in the direction of the longer axis
+        if abs(dy) > abs(dx):
+            flipped = True
+            (dy, dx) = (dx, dy)
+            (py, px) = (px, py)
+            (oy, ox) = (ox, oy)
+
+        xinc = 1 if dx >= 0 else -1
+        yinc = 1 if dy >= 0 else -1
+        
+        einc = abs(float(dy)/dx)
+        err = einc - 1.0
+
+        y = py + (yinc if einc >= 0.5 else 0)
+
+        for x in xrange(px + xinc, ox, xinc):
+            (ly, lx) = (x, y) if flipped else (y, x)
+
+            if not self.loc(ly, lx).transparent():
+                return False
+
+            err += einc
+            if err >= 0.5:
+                y += yinc
+                err -= 1.0
+
+        return True
+
+    # TODO: use dynamic programming: if we can't see a near tile, there is
+    # no reason to test the tiles behind it.
+    def draw(self):
+        if self.needs_full_repaint:
+            self.invalid_tiles = set((y,x) for y in range(self.h) for x in range(self.w))
+            self.needs_full_repaint = False
+        vis = set()
+        
+        for pl in self.players():
+            vis |= set(pl.visibles())
+
+        vis &= (self.invalid_tiles | self.active_tiles)
+        vis |= self.important_repaints
+        for (y,x) in vis:
+            (char, color) = self.grid[y][x].render()
+            self.boardwin.addstr(y, x, char, color)
+        self.invalid_tiles -= vis
+        self.important_repaints.clear()
+
+    def export(self):
+        extras = {}
+        f = StringIO()
+        f.write("#!RgLkLvL\n")
+        f.write("%d %d %s\n" % (self.h, self.w, self.name))
+        for i in range(self.h):
+            for j in range(self.w):
+                tile = self.grid[i][j]
+                (s, extra) = tile.export()
+                f.write(s)
+                if len(extra) > 0:
+                    extras[(i,j)] = extra
+            f.write("\n")
+        for ((y, x), ex) in extras.iteritems():
+            for (k,v) in ex.iteritems():
+                f.write("%d %d %s %s %s\n" % (y, x, k, type(v).__name__, v))
+        val = f.getvalue()
+        f.close()
+        return val
+
+    def level(self):
+        return self
+
+    def affect_all(self):
+        for (y,x) in self.active_tiles:
+            self.grid[y][x].affect_all()
+
+
+class LevelProxy (object):
+    def __init__(self, name):
+        self.name = name
+        self.lvl = None
+
+    def level(self):
+        if not self.lvl:
+            self.lvl = Level("levels/" + self.name + ".sav")
+        return self.lvl
+
diff --git a/loc.py b/loc.py
new file mode 100644 (file)
index 0000000..e28f683
--- /dev/null
+++ b/loc.py
@@ -0,0 +1,347 @@
+import curses
+import random
+import math
+
+import level
+from ui import Color
+
+class Location (object):
+    def __init__(self, container = None):
+        self._items = []      # in order from top to bottom
+        self._creatures = []
+        self._scenery = []
+        self._parent = container
+    def num_items(self):
+        return len(self._items)
+    def num_creatures(self):
+        return len(self._creatures)
+    def num_scenery(self):
+        return len(self._scenery)
+    def remove(self, obj):
+        if obj.is_item():
+            self._items.remove(obj)
+        elif obj.is_creature():
+            self._creatures.remove(obj)
+        elif obj.is_scenery():
+            self._scenery.remove(obj)
+    def add(self, obj):
+        if obj.is_item():
+            self._items[:0] = [obj]
+        elif obj.is_creature():
+            self._creatures[:0] = [obj]
+        elif obj.is_scenery():
+            self._scenery[:0] = [obj]
+    def top_item(self):
+        return self._items[0] if len(self._items) > 0 else None
+    def top_creature(self):
+        return self._creatures[0] if len(self._creatures) > 0 else None
+    def top_scenery(self):
+        return self._scenery[0] if len(self._scenery) > 0 else None
+    def container(self):
+        return self._parent
+    def level(self):
+        return self.container().level()
+    def can_hold(self, thing):
+        return False
+
+class Inventory (Location):
+    def __init__(self, creature):
+        Location.__init__(self, creature)
+    def __getitem__(self, k):
+        return self._items[k]
+    def can_hold(self, thing):
+        return thing.is_item()
+
+class Tile (Location):
+    "A square big enough for a player; the smallest unit of space."
+    def __init__(self, lvl, y, x):
+        Location.__init__(self, lvl)
+        (self.y, self.x) = (y, x)
+    def passable_by(self, thing):
+        return None
+    def transparent(self):
+        return True
+    def can_hold(self, thing):
+        return self.passable_by(thing)
+    def affect(self, thing):
+        pass
+    def close(self):
+        return False
+    def open(self):
+        return False
+    def invalidate(self, important=False):
+        self._parent.invalidate(self, important)
+    def add(self, obj):
+        Location.add(self, obj)
+        obj.invalidate()
+    def remove(self, obj):
+        obj.invalidate()
+        Location.remove(self,obj)
+    def target(self):
+        return None
+    def traverse(self):
+        return self.target()
+    def is_active(self):
+        return False
+    def affect_all(self):
+        for i in self._items:
+            self.affect(i)
+        for i in self._creatures:
+            self.affect(i)
+        for i in self._scenery:
+            self.affect(i)
+    def is_adjacent(self, tile):
+        return (self.level() == tile.level()
+                and abs(self.y - tile.y) <= 1
+                and abs(self.x - tile.x) <= 1)
+    def setattr(self, attr, value):
+        raise AttributeError("unknown attribute %s for %s" % (attr, self))
+    def open(self):
+        return False
+    def north(self, n=1):
+        newy = self.y - n
+        if newy >= 0:
+            return self.level().loc(newy, self.x)
+        else:
+            return None
+    def south(self, n=1):
+        newy = self.y + n
+        if newy < self.level().h:
+            return self.level().loc(newy, self.x)
+        else:
+            return None
+    def west(self, n=1):
+        newx = self.x - n
+        if newx >= 0:
+            return self.level().loc(self.y, newx)
+        else:
+            return None
+    def east(self, n=1):
+        newx = self.x + n
+        if newx < self.level().w:
+            return self.level().loc(self.y, newx)
+        else:
+            return None
+    def dir(self, key, n=1):
+        if key == curses.KEY_UP:
+            return self.north(n)
+        if key == curses.KEY_DOWN:
+            return self.south(n)
+        if key == curses.KEY_LEFT:
+            return self.west(n)
+        if key == curses.KEY_RIGHT:
+            return self.east(n)
+    def throwdest(self, key, obj, maxdist=6):
+        for i in range(maxdist+1):
+            loc = self.dir(key, i)
+            if loc == None or not loc.passable_by(obj):
+                if i>0:
+                    return self.dir(key, i-1)
+                else:
+                    return None
+        return self.dir(key, maxdist)
+    def render_bg(self):
+        return self.pic
+    def render(self):
+        pic = self.render_bg()
+        if self.num_scenery() > 0:
+            pic = self.top_scenery().render(pic)
+        if self.num_items() > 0:
+            pic = self.top_item().render(pic)
+        if self.num_creatures() > 0:
+            pic = self.top_creature().render(pic)
+        return pic
+
+class Floor(Tile):
+    "A passable Tile."
+    def __init__(self, lvl, y, x, objlist=[]):
+        Tile.__init__(self, lvl, y, x)
+        self.pic = random.choice("  ."), random.choice((
+            Color('BrightBlack').cp, Color('Brown').cp,
+            Color('BrightGreen').cp, Color('Green').cp,
+            ))
+        for o in objlist:
+            o.place(self)
+    def passable_by(self, thing):
+        return True
+    def export(self):
+        return (" ", {})
+
+class Stair(Floor):
+    "A tile that leads to another level."
+    tgt = None
+    oneway = False
+    def __init__(self, lvl, y, x, tgt = None, oneway = False, objlist=[]):
+        Floor.__init__(self, lvl, y, x, objlist)
+        self.pic = ">", Color('White').cp
+        if tgt:
+            self.tgt = tgt
+        if oneway:
+            self.oneway = oneway
+
+    def setattr(self, attr, value):
+        was_available = not (self.tgt or self.oneway)
+        if not attr in ("oneway", "tgt"):
+            raise Exception("Unknown attr %s for %s" % (attr, self.__class__))
+
+        self.__setattr__(attr, value)
+
+        is_available = not (self.tgt or self.oneway)
+
+        if was_available and not is_available:
+            self.level().stair_unavailable(self)
+        elif not was_available and is_available:
+            self.level().stair_available(self)
+
+    def target(self):
+        return self.tgt
+
+    def traverse(self):
+        oldlvl = self.level()
+        tgt = self.target()
+
+        if isinstance(tgt, Location):
+            return tgt
+
+        if tgt == None:
+            tgt = level.Level(None, height=40, width=90)
+        elif isinstance(tgt, level.LevelProxy):
+            tgt = tgt.level()
+
+        if isinstance(tgt, level.Level):
+            tgt = tgt.get_target(self.oneway)
+            if not self.oneway:
+                tgt.setattr("tgt", self)
+        elif isinstance(tgt, level.LocationProxy):
+            tgt = tgt.location()
+            if not self.oneway:
+                tgt.setattr("tgt", self)
+
+        assert isinstance(tgt, Location), (
+                "Target of %s is not a Location: %s" % (self, tgt))
+        self.tgt = tgt
+        return tgt
+
+    def export(self):
+        return (">", {'oneway': self.oneway, 'tgt': self.tgt})
+
+class Door(Tile):
+    "A Tile that may be opened and closed."
+    def __init__(self, lvl, y, x, opened = False, hidden = False):
+        Tile.__init__(self, lvl, y, x)
+        self.opened = opened
+        self.hidden = hidden
+        self.pic = self.getpic()
+    def transparent(self):
+        return self.opened
+    def getpic(self):
+        if self.hidden:
+            return "=" if self.opened else "#", Color('Default').cp
+        else:
+            return "-" if self.opened else "+", Color('Brown').cp
+    def open(self):
+        if self.opened:
+            return False
+        else:
+            self.opened = True
+            self.pic = self.getpic()
+            self.invalidate()
+            return True
+    def close(self):
+        if self.opened:
+            self.opened = False
+            self.pic = self.getpic()
+            self.invalidate()
+            return True
+        else:
+            return False
+    def hide(self):
+        if self.hidden:
+            return False
+        else:
+            self.hidden = True
+            self.pic = self.getpic()
+            self.invalidate()
+            return True
+    def unhide(self):
+        if self.hidden:
+            self.hidden = False
+            self.pic = self.getpic()
+            self.invalidate()
+            return True
+        else:
+            return False
+    def setattr(self, attr, value):
+        if attr == 'hidden':
+            self.hide() if value else self.unhide()
+        elif attr == 'opened':
+            self.open() if value else self.hide()
+        else:
+            raise Exception("Unknown attr %s for %s" % (attr, self.__class__))
+    def passable_by(self, thing):
+        return self.opened
+    def export(self):
+        return "-" if self.opened else "+", {'hidden': self.hidden, 'opened': self.opened}
+
+class Wall(Tile):
+    "An impassable Tile."
+    def __init__(self, lvl, y, x):
+        Tile.__init__(self, lvl, y, x)
+        self.pic = "#", Color('Default').cp
+    def transparent(self):
+        return False
+    def passable_by(self, thing):
+        return False
+    def export(self):
+        return ("#", {})
+
+class Water(Tile):
+    "A wet, mostly impassable, Tile."
+    def __init__(self, lvl, y, x, flowy=0.0, flowx=0.0):
+        Tile.__init__(self, lvl, y, x)
+        self.flowy = flowy
+        self.flowx = flowx
+        pic = ('~', Color('Blue').cp)
+
+    def set_flow(self, flowy, flowx):
+        self.flowy = flowy
+        self.flowx = flowx
+
+    def render_bg(self):
+        return ('~', random.choice((
+            Color('Blue').cp, Color('Blue').cp, Color('Blue').cp,
+            Color('Cyan').cp, Color('BrightBlue').cp, Color('BrightBlue').cp,
+            Color('BrightCyan').cp, Color('White').cp,
+            )))
+
+    def setattr(self, attr, value):
+        if not attr in ("flowy", "flowx"):
+            raise Exception("Unknown attr %s for %s" % (attr, self.__class__))
+        self.__setattr__(attr, value)
+
+    def passable_by(self, thing):
+        return thing.is_item()  or  thing.is_creature() and thing.can_swim()
+    
+    def is_active(self):
+        return True
+   
+    def affect(self, thing):
+        if random.random() < 0.5:
+            (fxf,fxw) = math.modf(self.flowx)
+            (fyf,fyw) = math.modf(self.flowy)
+            xs = 1 if self.flowx > 0.0 else -1
+            ys = 1 if self.flowy > 0.0 else -1
+            
+
+            newx = self.x + int(fxw)
+            newy = self.y + int(fyw)
+            if random.random() < abs(fxf):
+                newx += xs
+            if random.random() < abs(fyf):
+                newy += ys
+            (newy,newx) = self.level().clip(newy,newx)
+            thing.try_move(self.level().loc(newy,newx))
+
+    def export(self):
+        return ("~", { 'flowx' : self.flowx, 'flowy' : self.flowy })
+
index e459972..5e8f3a6 100755 (executable)
 
 from cStringIO import StringIO
 import random
-import codecs
-import sys
 import curses
-import _curses
 import curses.ascii
 import curses.panel
-import math
-import re
-import os.path
 import cProfile
 
-import cacher
+import things
+import level
+import ui
 
 do_profile = False
-cp = curses.color_pair
-
-
-class Color (object):
-    __metaclass__ = cacher.FirstArg
-    colors = {
-            'Default':       (curses.COLOR_WHITE,),
-            'Bright':        (curses.COLOR_WHITE,curses.A_BOLD),
-            'Red':           (curses.COLOR_RED,),
-            'Green':         (curses.COLOR_GREEN,),
-            'Brown':         (curses.COLOR_YELLOW,),
-            'Blue':          (curses.COLOR_BLUE,),
-            'Magenta':       (curses.COLOR_MAGENTA,),
-            'Cyan':          (curses.COLOR_CYAN,),
-            'Gray':          (curses.COLOR_WHITE,),
-            'Grey':          (curses.COLOR_WHITE,),
-            'Black':         (curses.COLOR_BLACK,),
-            'BrightRed':     (curses.COLOR_RED, curses.A_BOLD),
-            'BrightGreen':   (curses.COLOR_GREEN, curses.A_BOLD),
-            'BrightBrown':   (curses.COLOR_YELLOW, curses.A_BOLD),
-            'Yellow':        (curses.COLOR_YELLOW, curses.A_BOLD),
-            'BrightBlue':    (curses.COLOR_BLUE, curses.A_BOLD),
-            'BrightMagenta': (curses.COLOR_MAGENTA, curses.A_BOLD),
-            'BrightCyan':    (curses.COLOR_CYAN, curses.A_BOLD),
-            'BrightGray':    (curses.COLOR_WHITE, curses.A_BOLD),
-            'BrightGrey':    (curses.COLOR_WHITE, curses.A_BOLD),
-            'White':         (curses.COLOR_WHITE, curses.A_BOLD),
-            'BrightBlack':   (curses.COLOR_BLACK, curses.A_BOLD),
-            'DarkGray':      (curses.COLOR_BLACK, curses.A_BOLD),
-            'DarkGrey':      (curses.COLOR_BLACK, curses.A_BOLD),
-            }
-    npairs = 1
-    def __init__(self, color):
-        if isinstance(color, str):
-            fg = self.colors[color]
-            bg = self.colors['Black']
-        elif isinstance(color, int):
-            fg = color
-            bg = self.colors['Black']
-        else:
-            (fg,bg) = color
-            if isinstance(fg, str):
-                fg = self.colors[fg]
-            if isinstance(bg, str):
-                bg = self.colors[bg]
-
-        self.fg = fg[0]
-        (self.attrs) = fg[1] if len(fg) > 1 else 0
-        self.bg = bg[0]
-        try:
-            curses.init_pair(Color.npairs, self.fg, self.bg)
-        except:
-            raise Exception("%s %s %s %s" % (Color.npairs, fg, bg, color))
-        self.cp = curses.color_pair(Color.npairs) | self.attrs
-        Color.npairs += 1
-
-class LevelProxy (object):
-    def __init__(self, name):
-        self.name = name
-        self.lvl = None
-
-    def level(self):
-        if not self.lvl:
-            self.lvl = Level("levels/" + self.name + ".sav")
-        return self.lvl
-
-class LocationProxy (object):
-    def __init__(self, level=None, y=None, x=None, locstr=None):
-        if locstr:
-            (lstr, ystr, xstr) = locstr.split(None, 2)
-            level = LevelProxy(lstr)
-            y = int(ystr)
-            x = int(xstr)
-        assert (level and y is not None and x is not None)
-        self.lvl = level
-        self.y = y
-        self.x = x
-
-    def level(self):
-        if isinstance(self.lvl, LevelProxy):
-            self.lvl = self.lvl.level()
-        return self.lvl
-
-    def location(self):
-        return self.level().loc(self.y, self.x)
-
-
-class Level (object):
-    """Represents an area of the map comprising a contiguous rectangular
-    region of Tiles surrounded by an impassable boundary.  Levels may
-    be travelled between via transport points.
-    """
-    __metaclass__ = cacher.FirstArg
-
-    def stair_unavailable(self, stair):
-        if stair in self.unconnected_stairs:
-            self.unconnected_stairs.remove(stair)
-    def stair_available(self, stair):
-        self.unconnected_stairs.add(stair)
-
-    def __init__(self, filename=None, height=None, width=None, name=None):
-        self.active_tiles = set()
-        self.invalid_tiles = set()
-        self.important_repaints = set()
-        self.unconnected_stairs = set()
-        self.unconnected_drops = []
-        self.needs_full_repaint = True
-        self.filename = None
-
-        self.pick_tile = random.choice((self.pick_tile_1, self.pick_tile))
-
-        if filename:
-            assert height == width == name == None, \
-                    "Level constructor specified more than just the filename"
-            self.load_file(open(filename, "r"))
-        else:
-            assert height and width
-            self.h = height
-            self.w = width
-            self.name = name or ""
-
-            self.grid = [ [ self.activate(self.pick_tile(i,j))
-                for j in xrange(self.w) ]
-                for i in xrange(self.h) ]
-
-        random.shuffle(self.unconnected_drops)
-
-        self.boardwin = curses.newwin(self.h + 1, self.w + 1)
-    
-    def activate(self, tile):
-        (y, x) = (tile.y, tile.x)
-        if tile.is_active():
-            self.active_tiles.add((y, x))
-        if isinstance(tile, Stair) and not tile.target() and not tile.oneway:
-            self.stair_available(tile)
-        return tile
-
-    def load_file(self, f):
-        attrs = {}
-        def parse_line(line, lno):
-            linere = r'^(\d+)\s+(\d+)\s+(\w+)\s+(\w+)\s+(.*?)\s*\n$'
-            m = re.match(linere, line)
-            (y, x, attr, typ, valstr) = m.groups()
-            y = int(y) ; x = int(x)
-            if not 0 <= y < self.h:
-                raise Exception("extra out of bounds: y=%d" % (y))
-            if not 0 <= x < self.w:
-                raise Exception("extra out of bounds: x=%d" % (x))
-
-            val = None
-            if typ == "float":
-                val = float(valstr)
-            elif typ == "int":
-                val = int(valstr)
-            elif typ == "bool":
-                val = valstr != "False"
-            elif typ == "str":
-                val = valstr.strip()
-            elif typ == "level":
-                val = LevelProxy(valstr.strip())
-            elif typ == "loc":
-                val = LocationProxy(locstr = valstr.strip())
-            else:
-                raise Exception("unknown type %s (val '%s')" % (typ, valstr))
-            if (y,x) not in attrs:
-                attrs[(y,x)] = {}
-            attrs[(y,x)][attr] = val
-
-        line = f.readline() ; lno = 1
-        if line.rstrip('\n') != "#!RgLkLvL":
-            raise Exception("Bad first line: '%s'" % (line))
-
-        line = f.readline() ; lno = 2
-
-        (hs, ws, name) = re.split("\\s+", line, 2)
-        self.h = int(hs)
-        self.w = int(ws)
-        self.name = name.rstrip('\n')
-
-        self.grid = [[None] * self.w for x in xrange(self.h)]
-
-        lines = []
-
-        first_lno = lno + 1
-        for i in range(self.h):
-            lines.append(f.readline())
-            lno = lno + 1
-
-        for l in f.readlines():
-            lno = lno + 1
-            parse_line(l, lno)
-
-        for i in range(self.h):
-            line = lines[i]
-            if len(line) != self.w + 1:
-                raise Exception("incorrect line length: line %d = %d"
-                        % (first_lno + i, len(line)))
-            for j in range(self.w):
-                t = self.decode_tile(i, j, line[j],
-                        attrs[(i,j)] if (i,j) in attrs else {})
-                self.grid[i][j] = self.activate(t)
-            if line[self.w] != '\n':
-                raise Exception("line %d has no newline" % (i))
-
-
-        f.close()
-
-    def pick_tile_1(self, i, j):
-        if i == 15 and j == 60:
-            return Stair(self, i, j)
-        elif (i-15)**2 + ((j-60)**2)/2 <= 36:
-            flowy = (j-60) / (math.sqrt(2) * 6) - (i-15)/(math.sqrt(2)*18)
-            flowx = (15-i) * math.sqrt(2) / 6 - (j-60)*math.sqrt(2)/18
-            return Water(self, i, j, flowy, flowx)
-        elif 580 < (i-5)**2 + ((j-45)**2)/2 < 900:
-            flowy = (45-j) / (math.sqrt(2) * 27)
-            flowx = (i-5) * math.sqrt(2) / 27
-            return Water(self, i, j, flowy, flowx)
-        elif random.random() < .1:
-            return Wall(self, i, j)
-        else:
-            return Floor(self, i, j, self.randitems())
-    
-    def pick_tile(self, i, j):
-        if i==0 or j==0 or i == self.h - 1 or j == self.w - 1:
-            return Wall(self, i, j)
-        if (i == 5 and j == 9) or (i == self.h-5 and j == self.w - 10):
-            return Stair(self, i, j)
-        if i % 9 == 0:
-            if (j == 9 and i % 18 == 0) or (j == self.w-10 and i % 18 == 9):
-                return Door(self, i, j)
-            else:
-                return Wall(self, i, j)
-        if j % 18 == 0:
-            return Wall(self, i, j) if i % 9 != 4 else Door(self, i, j)
-        else:
-            return Floor(self, i, j, self.randitems())
-
-    def get_target(self, oneway):
-        if oneway:
-            return self.unconnected_drops.pop()
-        else:
-            return self.unconnected_stairs.pop()
-
-    def invalidate(self, tile=None, important=False):
-        if tile == None:
-            self.needs_full_repaint = True
-        else:
-            assert tile.level() == self
-            self.invalid_tiles.add((tile.y, tile.x))
-            if important:
-                self.important_repaints.add((tile.y, tile.x))
-
-    def decode_tile(self, i, j, char, attrs):
-        if char == "#":
-            return Wall(self, i, j, **attrs)
-        elif char == " ":
-            return Floor(self, i, j, self.randitems(), **attrs)
-        elif char == "~":
-            return Water(self, i, j, **attrs)
-        elif char == "+":
-            return Door(self, i, j, opened = False, **attrs)
-        elif char == "-":
-            return Door(self, i, j, opened = True, **attrs)
-        elif char == ">":
-            return Stair(self, i, j, **attrs)
-        elif char == "<":
-            # Target for one-way stairs
-            tile = Floor(self, i, j, **attrs)
-            self.unconnected_drops.append(tile)
-            return tile
-        else:
-            raise Exception
-
-    def randitems(self):
-        r = 1000*random.random()
-        if r >= 10:
-            return []
-        elif r >= 5:
-            return [ ItemClass("gold piece")() ]
-        elif r >= 2:
-            return [ ItemClass("gem")() ]
-        elif r >= 1:
-            return [ ItemClass("shield")() ]
-        else:
-            return [ ItemClass("sword")() ]
-
-    def loc(self, i, j):
-        return self.grid[i][j]
-
-    def clip(self, y, x):
-        if y<0:
-            y = 0
-        elif y >= self.h:
-            y = self.h-1
-        if x<0:
-            x = 0
-        elif x >= self.w:
-            x = self.w-1
-        return (y, x)
-
-    def players(self):
-        return (pl for pl in AppUI.instance.players if pl.level() == self)
-
-    def visible_path(self, py, px, oy, ox):
-        (dy, dx) = (oy-py, ox-px)
-        if abs(dy) <= 1 and abs(dx) <= 1:
-            return True
-        flipped = False
-        # Go in the direction of the longer axis
-        if abs(dy) > abs(dx):
-            flipped = True
-            (dy, dx) = (dx, dy)
-            (py, px) = (px, py)
-            (oy, ox) = (ox, oy)
-
-        xinc = 1 if dx >= 0 else -1
-        yinc = 1 if dy >= 0 else -1
-        
-        einc = abs(float(dy)/dx)
-        err = einc - 1.0
-
-        y = py + (yinc if einc >= 0.5 else 0)
-
-        for x in xrange(px + xinc, ox, xinc):
-            (ly, lx) = (x, y) if flipped else (y, x)
-
-            if not self.loc(ly, lx).transparent():
-                return False
-
-            err += einc
-            if err >= 0.5:
-                y += yinc
-                err -= 1.0
-
-        return True
-
-    # TODO: use dynamic programming: if we can't see a near tile, there is
-    # no reason to test the tiles behind it.
-    def draw(self):
-        if self.needs_full_repaint:
-            self.invalid_tiles = set((y,x) for y in range(self.h) for x in range(self.w))
-            self.needs_full_repaint = False
-        vis = set()
-        
-        for pl in self.players():
-            vis |= set(pl.visibles())
-
-        vis &= (self.invalid_tiles | self.active_tiles)
-        vis |= self.important_repaints
-        for (y,x) in vis:
-            (char, color) = self.grid[y][x].render()
-            self.boardwin.addstr(y, x, char, color)
-        self.invalid_tiles -= vis
-        self.important_repaints.clear()
-
-    def export(self):
-        extras = {}
-        f = StringIO()
-        f.write("#!RgLkLvL\n")
-        f.write("%d %d %s\n" % (self.h, self.w, self.name))
-        for i in range(self.h):
-            for j in range(self.w):
-                tile = self.grid[i][j]
-                (s, extra) = tile.export()
-                f.write(s)
-                if len(extra) > 0:
-                    extras[(i,j)] = extra
-            f.write("\n")
-        for ((y, x), ex) in extras.iteritems():
-            for (k,v) in ex.iteritems():
-                f.write("%d %d %s %s %s\n" % (y, x, k, type(v).__name__, v))
-        val = f.getvalue()
-        f.close()
-        return val
-
-    def level(self):
-        return self
-
-    def affect_all(self):
-        for (y,x) in self.active_tiles:
-            self.grid[y][x].affect_all()
-
-class Location (object):
-    def __init__(self, container = None):
-        self._items = []      # in order from top to bottom
-        self._creatures = []
-        self._scenery = []
-        self._parent = container
-    def num_items(self):
-        return len(self._items)
-    def num_creatures(self):
-        return len(self._creatures)
-    def num_scenery(self):
-        return len(self._scenery)
-    def remove(self, obj):
-        if obj.is_item():
-            self._items.remove(obj)
-        elif obj.is_creature():
-            self._creatures.remove(obj)
-        elif obj.is_scenery():
-            self._scenery.remove(obj)
-    def add(self, obj):
-        if obj.is_item():
-            self._items[:0] = [obj]
-        elif obj.is_creature():
-            self._creatures[:0] = [obj]
-        elif obj.is_scenery():
-            self._scenery[:0] = [obj]
-    def top_item(self):
-        return self._items[0] if len(self._items) > 0 else None
-    def top_creature(self):
-        return self._creatures[0] if len(self._creatures) > 0 else None
-    def top_scenery(self):
-        return self._scenery[0] if len(self._scenery) > 0 else None
-    def container(self):
-        return self._parent
-    def level(self):
-        return self.container().level()
-    def can_hold(self, thing):
-        return False
-
-class Inventory (Location):
-    def __init__(self, creature):
-        Location.__init__(self, creature)
-    def __getitem__(self, k):
-        return self._items[k]
-    def can_hold(self, thing):
-        return thing.is_item()
-
-class Tile (Location):
-    "A square big enough for a player; the smallest unit of space."
-    def __init__(self, lvl, y, x):
-        Location.__init__(self, lvl)
-        (self.y, self.x) = (y, x)
-    def passable_by(self, thing):
-        return None
-    def transparent(self):
-        return True
-    def can_hold(self, thing):
-        return self.passable_by(thing)
-    def affect(self, thing):
-        pass
-    def close(self):
-        return False
-    def open(self):
-        return False
-    def invalidate(self, important=False):
-        self._parent.invalidate(self, important)
-    def add(self, obj):
-        Location.add(self, obj)
-        obj.invalidate()
-    def remove(self, obj):
-        obj.invalidate()
-        Location.remove(self,obj)
-    def target(self):
-        return None
-    def traverse(self):
-        return self.target()
-    def is_active(self):
-        return False
-    def affect_all(self):
-        for i in self._items:
-            self.affect(i)
-        for i in self._creatures:
-            self.affect(i)
-        for i in self._scenery:
-            self.affect(i)
-    def is_adjacent(self, tile):
-        return (self.level() == tile.level()
-                and abs(self.y - tile.y) <= 1
-                and abs(self.x - tile.x) <= 1)
-    def setattr(self, attr, value):
-        raise AttributeError("unknown attribute %s for %s" % (attr, self))
-    def open(self):
-        return False
-    def north(self, n=1):
-        newy = self.y - n
-        if newy >= 0:
-            return self.level().loc(newy, self.x)
-        else:
-            return None
-    def south(self, n=1):
-        newy = self.y + n
-        if newy < self.level().h:
-            return self.level().loc(newy, self.x)
-        else:
-            return None
-    def west(self, n=1):
-        newx = self.x - n
-        if newx >= 0:
-            return self.level().loc(self.y, newx)
-        else:
-            return None
-    def east(self, n=1):
-        newx = self.x + n
-        if newx < self.level().w:
-            return self.level().loc(self.y, newx)
-        else:
-            return None
-    def dir(self, key, n=1):
-        if key == curses.KEY_UP:
-            return self.north(n)
-        if key == curses.KEY_DOWN:
-            return self.south(n)
-        if key == curses.KEY_LEFT:
-            return self.west(n)
-        if key == curses.KEY_RIGHT:
-            return self.east(n)
-    def throwdest(self, key, obj, maxdist=6):
-        for i in range(maxdist+1):
-            loc = self.dir(key, i)
-            if loc == None or not loc.passable_by(obj):
-                if i>0:
-                    return self.dir(key, i-1)
-                else:
-                    return None
-        return self.dir(key, maxdist)
-    def render_bg(self):
-        return self.pic
-    def render(self):
-        pic = self.render_bg()
-        if self.num_scenery() > 0:
-            pic = self.top_scenery().render(pic)
-        if self.num_items() > 0:
-            pic = self.top_item().render(pic)
-        if self.num_creatures() > 0:
-            pic = self.top_creature().render(pic)
-        return pic
-
-class Floor(Tile):
-    "A passable Tile."
-    def __init__(self, lvl, y, x, objlist=[]):
-        Tile.__init__(self, lvl, y, x)
-        self.pic = random.choice("  ."), random.choice((
-            Color('BrightBlack').cp, Color('Brown').cp,
-            Color('BrightGreen').cp, Color('Green').cp,
-            ))
-        for o in objlist:
-            o.place(self)
-    def passable_by(self, thing):
-        return True
-    def export(self):
-        return (" ", {})
-
-class Stair(Floor):
-    "A tile that leads to another level."
-    tgt = None
-    oneway = False
-    def __init__(self, lvl, y, x, tgt = None, oneway = False, objlist=[]):
-        Floor.__init__(self, lvl, y, x, objlist)
-        self.pic = ">", Color('White').cp
-        if tgt:
-            self.tgt = tgt
-        if oneway:
-            self.oneway = oneway
-
-    def setattr(self, attr, value):
-        was_available = not (self.tgt or self.oneway)
-        if not attr in ("oneway", "tgt"):
-            raise Exception("Unknown attr %s for %s" % (attr, self.__class__))
-
-        self.__setattr__(attr, value)
-
-        is_available = not (self.tgt or self.oneway)
-
-        if was_available and not is_available:
-            self.level().stair_unavailable(self)
-        elif not was_available and is_available:
-            self.level().stair_available(self)
-
-    def target(self):
-        return self.tgt
-
-    def traverse(self):
-        oldlvl = self.level()
-        tgt = self.target()
-
-        if isinstance(tgt, Location):
-            return tgt
-
-        if tgt == None:
-            tgt = Level(None, height=40, width=90)
-        elif isinstance(tgt, LevelProxy):
-            tgt = tgt.level()
-
-        if isinstance(tgt, Level):
-            tgt = tgt.get_target(self.oneway)
-            if not self.oneway:
-                tgt.setattr("tgt", self)
-        elif isinstance(tgt, LocationProxy):
-            tgt = tgt.location()
-            if not self.oneway:
-                tgt.setattr("tgt", self)
-
-        assert isinstance(tgt, Location), (
-                "Target of %s is not a Location: %s" % (self, tgt))
-        self.tgt = tgt
-        return tgt
-
-    def export(self):
-        return (">", {'oneway': self.oneway, 'tgt': self.tgt})
-
-class Door(Tile):
-    "A Tile that may be opened and closed."
-    def __init__(self, lvl, y, x, opened = False, hidden = False):
-        Tile.__init__(self, lvl, y, x)
-        self.opened = opened
-        self.hidden = hidden
-        self.pic = self.getpic()
-    def transparent(self):
-        return self.opened
-    def getpic(self):
-        if self.hidden:
-            return "=" if self.opened else "#", Color('Default').cp
-        else:
-            return "-" if self.opened else "+", Color('Brown').cp
-    def open(self):
-        if self.opened:
-            return False
-        else:
-            self.opened = True
-            self.pic = self.getpic()
-            self.invalidate()
-            return True
-    def close(self):
-        if self.opened:
-            self.opened = False
-            self.pic = self.getpic()
-            self.invalidate()
-            return True
-        else:
-            return False
-    def hide(self):
-        if self.hidden:
-            return False
-        else:
-            self.hidden = True
-            self.pic = self.getpic()
-            self.invalidate()
-            return True
-    def unhide(self):
-        if self.hidden:
-            self.hidden = False
-            self.pic = self.getpic()
-            self.invalidate()
-            return True
-        else:
-            return False
-    def setattr(self, attr, value):
-        if attr == 'hidden':
-            self.hide() if value else self.unhide()
-        elif attr == 'opened':
-            self.open() if value else self.hide()
-        else:
-            raise Exception("Unknown attr %s for %s" % (attr, self.__class__))
-    def passable_by(self, thing):
-        return self.opened
-    def export(self):
-        return "-" if self.opened else "+", {'hidden': self.hidden, 'opened': self.opened}
-
-class Wall(Tile):
-    "An impassable Tile."
-    def __init__(self, lvl, y, x):
-        Tile.__init__(self, lvl, y, x)
-        self.pic = "#", Color('Default').cp
-    def transparent(self):
-        return False
-    def passable_by(self, thing):
-        return False
-    def export(self):
-        return ("#", {})
-
-class Water(Tile):
-    "A wet, mostly impassable, Tile."
-    def __init__(self, lvl, y, x, flowy=0.0, flowx=0.0):
-        Tile.__init__(self, lvl, y, x)
-        self.flowy = flowy
-        self.flowx = flowx
-        pic = ('~', Color('Blue').cp)
-
-    def set_flow(self, flowy, flowx):
-        self.flowy = flowy
-        self.flowx = flowx
-
-    def render_bg(self):
-        return ('~', random.choice((
-            Color('Blue').cp, Color('Blue').cp, Color('Blue').cp,
-            Color('Cyan').cp, Color('BrightBlue').cp, Color('BrightBlue').cp,
-            Color('BrightCyan').cp, Color('White').cp,
-            )))
-
-    def setattr(self, attr, value):
-        if not attr in ("flowy", "flowx"):
-            raise Exception("Unknown attr %s for %s" % (attr, self.__class__))
-        self.__setattr__(attr, value)
-
-    def passable_by(self, thing):
-        return thing.is_item()  or  thing.is_creature() and thing.can_swim()
-    
-    def is_active(self):
-        return True
-   
-    def affect(self, thing):
-        if random.random() < 0.5:
-            (fxf,fxw) = math.modf(self.flowx)
-            (fyf,fyw) = math.modf(self.flowy)
-            xs = 1 if self.flowx > 0.0 else -1
-            ys = 1 if self.flowy > 0.0 else -1
-            
-
-            newx = self.x + int(fxw)
-            newy = self.y + int(fyw)
-            if random.random() < abs(fxf):
-                newx += xs
-            if random.random() < abs(fyf):
-                newy += ys
-            (newy,newx) = self.level().clip(newy,newx)
-            thing.try_move(self.level().loc(newy,newx))
-
-    def export(self):
-        return ("~", { 'flowx' : self.flowx, 'flowy' : self.flowy })
-
-class Material(object):
-    __metaclass__ = cacher.FirstArg
-
-    def __init__(self, name, adj, attrs):
-        self.name = name
-        self.adj = adj or name
-        self.attrs = attrs
-    
-    @classmethod
-    def load(cls, fh):
-        for line in fh:
-            line = line.rstrip('\n')
-            name = None
-            adj = None
-            color = None
-            if re.match(r"#|\s*$", line):
-                continue
-            mat = re.match(
-                    r'\s*(?:(\w+)|"([^"]*)")(?:\s+\(([^()]*)\))?\s*:\s+(.*?)\s*$',
-                    line)
-            assert mat, "Material line '%s' did not match" % (line,)
-
-            (name, qname, adj, val) = mat.groups()
-
-            name = name or qname
-            assert name, "Material '%s' does not have a name" % (line)
-
-            while val:
-                vm = re.match(r'\s*(\w+)=(?:(\w+)|"([^"]*)")\s*', val)
-                assert vm, "Material value '%s' is bad" % (val,)
-
-                (vname, vval, qvval) = vm.groups()
-                vval = vval or qvval
-
-                if vname == 'color':
-                    color = vval
-                else:
-                    raise Exception("Unknown attribute '%s'" % (vname,))
-
-                val = val[vm.end():]
-
-            # Now we have the info we need
-            yield (Material(name, adj, color))
-
-    @classmethod
-    def loadall(cls, fh):
-        return list(cls.load(fh))
-
-    def attr(self):
-        if callable(self.attrs):
-            return self.attrs()
-        elif self.attrs is None:
-            self.attrs = Color('Default').cp
-        elif isinstance(self.attrs, str):
-            self.attrs = Color(self.attrs).cp
-
-        return self.attrs
-
-class ItemClass (type):
-    __metaclass__ = cacher.FirstArg
-    def __new__(cls, name, properties):
-        base = Item
-        props = properties.copy()
-        props['name'] = name
-        if 'material' in props and isinstance(props['material'], str):
-            props['material'] = Material(props['material'])
-        if 'isa' in props:
-            base = ItemClass(props['isa'])
-            del props['isa']
-        return type.__new__(cls, name + " item", (base,), props)
-
-    @classmethod 
-    def load(cls, fh):
-        import pyparsing
-        from pyparsing import Word, Suppress, Optional, Dict, Group
-        ident = Word( pyparsing.alphas, pyparsing.alphanums )
-        string = ident | pyparsing.QuotedString(
-                quoteChar = '"', escChar = '\\', unquoteResults = True)
-        value = string
-        attrib = Group(ident + Suppress(':') + string)
-        block = Suppress('{') + pyparsing.delimitedList(attrib, delim=',') + \
-                Suppress(Optional(',') + '}')
-        item = Group(string.setResultsName('name') +
-                Dict(block).setResultsName('props'))
-
-        itemlist = pyparsing.Dict(
-                pyparsing.delimitedList(item, delim=',') +
-                Suppress(Optional(',')))
-
-        for item in itemlist.parseFile(fh):
-            yield ( cls(item.name, item.props.asDict()) )
-    
-
-    @classmethod
-    def loadall(cls, fh):
-        return list(cls.load(fh))
-
-class Thing (object):
-    "An item, creature, or piece of scenery."
-    def __init__(self, location=None):
-        self.location = None
-        self.place(location)
-    def invalidate(self):
-        self.location.invalidate(important = False)
-    def is_creature(self):
-        return False
-    def is_item(self):
-        return False
-    def is_scenery(self):
-        return False
-    def level(self):
-        return self.location.level()
-    def place(self, location):
-        if self.location is not None:
-            self.location.remove(self)
-        self.location = location
-        if location is not None:
-            self.location.add(self)
-    def try_move(self, newloc):
-        if newloc and newloc.can_hold(self):
-            self.place(newloc)
-            return True
-        else:
-            return False
-
-class Item(Thing):
-    def is_item(self):
-        return True
-    def render(self, pic=None):
-        return (self.ascii, self.material.attr())
-
-class Creature(Thing):
-    def __init__(self, location=None):
-        Thing.__init__(self, location)
-    def vis_radius(self):
-        return 5
-    def can_see(self, y, x):
-        loc = self.location
-        if (y - loc.y)**2 + (x - loc.x)**2 <= self.vis_radius()**2:
-            return self.level().visible_path(loc.y, loc.x, y, x)
-        return False
-    def visibles(self):
-        vis = []
-        py = self.location.y
-        px = self.location.x
-        rad = self.vis_radius()
-        (ymin,xmin) = self.level().clip(py - rad, px - rad)
-        (ymax,xmax) = self.level().clip(py + rad, px + rad)
-        for y in range(ymin, ymax+1):
-            for x in range(xmin, xmax+1):
-                if self.can_see(y,x):
-                    vis.append((y,x))
-        return vis
-
-    def is_creature(self):
-        return True
-    def can_swim(self):
-        return False
-    def render(self, pic=None):
-        return ( "?", Color('BrightMagenta').cp )
-    def move_north(self):
-        return self.try_move(self.location.north())
-    def move_south(self):
-        return self.try_move(self.location.south())
-    def move_west(self):
-        return self.try_move(self.location.west())
-    def move_east(self):
-        return self.try_move(self.location.east())
-
-class Player(Creature):
-    def __init__(self, location=None):
-        Creature.__init__(self, location)
-        self.inv = Inventory(self)
-        self.active = False
-    def invalidate(self):
-        self.location.invalidate(important = True)
-    def render(self, pic=None):
-        return ("@", Color('BrightMagenta' if self.active else 'Magenta').cp)
-    def can_swim(self):
-        return random.random() < .5
-    def pick_up(self):
-        it = self.location.top_item()
-        if it:
-            it.try_move(self.inv)
-            return True
-        else:
-            return False
-
 
 
 class AppUI:
     instance = None
-    color_pairs = {}
-
-    @classmethod
-    def cp(cls, name):
-        return cls.color_pairs[name]
 
     def is_arrow(self, ch):
         return (ch == curses.KEY_RIGHT or ch == curses.KEY_LEFT
@@ -1058,11 +138,11 @@ class AppUI:
         win = self.invpanel.window()
         inv = self.invpanel.userptr()
         (h,w) = win.getmaxyx()
-        win.attrset(Color('Red').cp)
+        win.attrset(ui.Color('Red').cp)
         win.erase()
         win.border()
         win.addstr(0,2, " Inventory ")
-        win.attrset(Color('White').cp)
+        win.attrset(ui.Color('White').cp)
         if inv != None:
             for i in range(min(h-2, inv.num_items())):
                 item = inv[i]
@@ -1190,22 +270,23 @@ class AppUI:
 
             self.level().affect_all()
 
-def main(stdscr):
-    me = Player()
-    him = Player()
-    ui = AppUI(stdscr, [me, him])
+if __name__ == '__main__':
+    def main(stdscr):
+        me = things.Player()
+        him = things.Player()
+        ui = AppUI(stdscr, [me, him])
 
-    Material.loadall(open("materials/materials.mtl"))
-    ItemClass.loadall(open("items/items.itm"))
+        things.Material.loadall(open("materials/materials.mtl"))
+        things.ItemClass.loadall(open("items/items.itm"))
 
-    lvl = Level("levels/level0.sav")
-    me.place(lvl.loc(min(10, lvl.h - 2), min(5, lvl.w - 2)))
-    him.place(lvl.loc(max(2, lvl.h - 5), max(5, lvl.w - 10)))
-    ui.set_level(lvl)
+        lvl = level.Level("levels/level0.sav")
+        me.place(lvl.loc(min(10, lvl.h - 2), min(5, lvl.w - 2)))
+        him.place(lvl.loc(max(2, lvl.h - 5), max(5, lvl.w - 10)))
+        ui.set_level(lvl)
 
-    ui.event_loop()
+        ui.event_loop()
 
-if do_profile:
-    cProfile.run('curses.wrapper(main)', 'roguelike.prof')
-else:
-    curses.wrapper(main)
+    if do_profile:
+        cProfile.run('curses.wrapper(main)', 'roguelike.prof')
+    else:
+        curses.wrapper(main)
diff --git a/things.py b/things.py
new file mode 100644 (file)
index 0000000..b33637a
--- /dev/null
+++ b/things.py
@@ -0,0 +1,195 @@
+import random
+import re
+
+import loc
+from ui import Color
+
+import cacher
+
+class Material(object):
+    __metaclass__ = cacher.FirstArg
+
+    def __init__(self, name, adj, attrs):
+        self.name = name
+        self.adj = adj or name
+        self.attrs = attrs
+    
+    @classmethod
+    def load(cls, fh):
+        for line in fh:
+            line = line.rstrip('\n')
+            name = None
+            adj = None
+            color = None
+            if re.match(r"#|\s*$", line):
+                continue
+            mat = re.match(
+                    r'\s*(?:(\w+)|"([^"]*)")(?:\s+\(([^()]*)\))?\s*:\s+(.*?)\s*$',
+                    line)
+            assert mat, "Material line '%s' did not match" % (line,)
+
+            (name, qname, adj, val) = mat.groups()
+
+            name = name or qname
+            assert name, "Material '%s' does not have a name" % (line)
+
+            while val:
+                vm = re.match(r'\s*(\w+)=(?:(\w+)|"([^"]*)")\s*', val)
+                assert vm, "Material value '%s' is bad" % (val,)
+
+                (vname, vval, qvval) = vm.groups()
+                vval = vval or qvval
+
+                if vname == 'color':
+                    color = vval
+                else:
+                    raise Exception("Unknown attribute '%s'" % (vname,))
+
+                val = val[vm.end():]
+
+            # Now we have the info we need
+            yield (Material(name, adj, color))
+
+    @classmethod
+    def loadall(cls, fh):
+        return list(cls.load(fh))
+
+    def attr(self):
+        if callable(self.attrs):
+            return self.attrs()
+        elif self.attrs is None:
+            self.attrs = Color('Default').cp
+        elif isinstance(self.attrs, str):
+            self.attrs = Color(self.attrs).cp
+
+        return self.attrs
+
+class ItemClass (type):
+    __metaclass__ = cacher.FirstArg
+    def __new__(cls, name, properties):
+        base = Item
+        props = properties.copy()
+        props['name'] = name
+        if 'material' in props and isinstance(props['material'], str):
+            props['material'] = Material(props['material'])
+        if 'isa' in props:
+            base = ItemClass(props['isa'])
+            del props['isa']
+        return type.__new__(cls, name + " item", (base,), props)
+
+    @classmethod 
+    def load(cls, fh):
+        import pyparsing
+        from pyparsing import Word, Suppress, Optional, Dict, Group
+        ident = Word( pyparsing.alphas, pyparsing.alphanums )
+        string = ident | pyparsing.QuotedString(
+                quoteChar = '"', escChar = '\\', unquoteResults = True)
+        value = string
+        attrib = Group(ident + Suppress(':') + string)
+        block = Suppress('{') + pyparsing.delimitedList(attrib, delim=',') + \
+                Suppress(Optional(',') + '}')
+        item = Group(string.setResultsName('name') +
+                Dict(block).setResultsName('props'))
+
+        itemlist = pyparsing.Dict(
+                pyparsing.delimitedList(item, delim=',') +
+                Suppress(Optional(',')))
+
+        for item in itemlist.parseFile(fh):
+            yield ( cls(item.name, item.props.asDict()) )
+    
+
+    @classmethod
+    def loadall(cls, fh):
+        return list(cls.load(fh))
+
+class Thing (object):
+    "An item, creature, or piece of scenery."
+    def __init__(self, location=None):
+        self.location = None
+        self.place(location)
+    def invalidate(self):
+        self.location.invalidate(important = False)
+    def is_creature(self):
+        return False
+    def is_item(self):
+        return False
+    def is_scenery(self):
+        return False
+    def level(self):
+        return self.location.level()
+    def place(self, location):
+        if self.location is not None:
+            self.location.remove(self)
+        self.location = location
+        if location is not None:
+            self.location.add(self)
+    def try_move(self, newloc):
+        if newloc and newloc.can_hold(self):
+            self.place(newloc)
+            return True
+        else:
+            return False
+
+class Item(Thing):
+    def is_item(self):
+        return True
+    def render(self, pic=None):
+        return (self.ascii, self.material.attr())
+
+class Creature(Thing):
+    def __init__(self, location=None):
+        Thing.__init__(self, location)
+    def vis_radius(self):
+        return 5
+    def can_see(self, y, x):
+        loc = self.location
+        if (y - loc.y)**2 + (x - loc.x)**2 <= self.vis_radius()**2:
+            return self.level().visible_path(loc.y, loc.x, y, x)
+        return False
+    def visibles(self):
+        vis = []
+        py = self.location.y
+        px = self.location.x
+        rad = self.vis_radius()
+        (ymin,xmin) = self.level().clip(py - rad, px - rad)
+        (ymax,xmax) = self.level().clip(py + rad, px + rad)
+        for y in range(ymin, ymax+1):
+            for x in range(xmin, xmax+1):
+                if self.can_see(y,x):
+                    vis.append((y,x))
+        return vis
+
+    def is_creature(self):
+        return True
+    def can_swim(self):
+        return False
+    def render(self, pic=None):
+        return ( "?", Color('BrightMagenta').cp )
+    def move_north(self):
+        return self.try_move(self.location.north())
+    def move_south(self):
+        return self.try_move(self.location.south())
+    def move_west(self):
+        return self.try_move(self.location.west())
+    def move_east(self):
+        return self.try_move(self.location.east())
+
+class Player(Creature):
+    def __init__(self, location=None):
+        Creature.__init__(self, location)
+        self.inv = loc.Inventory(self)
+        self.active = False
+    def invalidate(self):
+        self.location.invalidate(important = True)
+    def render(self, pic=None):
+        return ("@", Color('BrightMagenta' if self.active else 'Magenta').cp)
+    def can_swim(self):
+        return random.random() < .5
+    def pick_up(self):
+        it = self.location.top_item()
+        if it:
+            it.try_move(self.inv)
+            return True
+        else:
+            return False
diff --git a/ui.py b/ui.py
new file mode 100644 (file)
index 0000000..fc3cf68
--- /dev/null
+++ b/ui.py
@@ -0,0 +1,55 @@
+import curses
+import cacher
+
+class Color (object):
+    __metaclass__ = cacher.FirstArg
+    colors = {
+            'Default':       (curses.COLOR_WHITE,),
+            'Bright':        (curses.COLOR_WHITE,curses.A_BOLD),
+            'Red':           (curses.COLOR_RED,),
+            'Green':         (curses.COLOR_GREEN,),
+            'Brown':         (curses.COLOR_YELLOW,),
+            'Blue':          (curses.COLOR_BLUE,),
+            'Magenta':       (curses.COLOR_MAGENTA,),
+            'Cyan':          (curses.COLOR_CYAN,),
+            'Gray':          (curses.COLOR_WHITE,),
+            'Grey':          (curses.COLOR_WHITE,),
+            'Black':         (curses.COLOR_BLACK,),
+            'BrightRed':     (curses.COLOR_RED, curses.A_BOLD),
+            'BrightGreen':   (curses.COLOR_GREEN, curses.A_BOLD),
+            'BrightBrown':   (curses.COLOR_YELLOW, curses.A_BOLD),
+            'Yellow':        (curses.COLOR_YELLOW, curses.A_BOLD),
+            'BrightBlue':    (curses.COLOR_BLUE, curses.A_BOLD),
+            'BrightMagenta': (curses.COLOR_MAGENTA, curses.A_BOLD),
+            'BrightCyan':    (curses.COLOR_CYAN, curses.A_BOLD),
+            'BrightGray':    (curses.COLOR_WHITE, curses.A_BOLD),
+            'BrightGrey':    (curses.COLOR_WHITE, curses.A_BOLD),
+            'White':         (curses.COLOR_WHITE, curses.A_BOLD),
+            'BrightBlack':   (curses.COLOR_BLACK, curses.A_BOLD),
+            'DarkGray':      (curses.COLOR_BLACK, curses.A_BOLD),
+            'DarkGrey':      (curses.COLOR_BLACK, curses.A_BOLD),
+            }
+    npairs = 1
+    def __init__(self, color):
+        if isinstance(color, str):
+            fg = self.colors[color]
+            bg = self.colors['Black']
+        elif isinstance(color, int):
+            fg = color
+            bg = self.colors['Black']
+        else:
+            (fg,bg) = color
+            if isinstance(fg, str):
+                fg = self.colors[fg]
+            if isinstance(bg, str):
+                bg = self.colors[bg]
+
+        self.fg = fg[0]
+        (self.attrs) = fg[1] if len(fg) > 1 else 0
+        self.bg = bg[0]
+        try:
+            curses.init_pair(Color.npairs, self.fg, self.bg)
+        except:
+            raise Exception("%s %s %s %s" % (Color.npairs, fg, bg, color))
+        self.cp = curses.color_pair(Color.npairs) | self.attrs
+        Color.npairs += 1