<offset>: Parse, evaluate, and example test case. (Fixes issue #3)
[python-bulletml.git] / bulletml / parser.py
index b39437a..a07cc2d 100644 (file)
@@ -1,6 +1,10 @@
 """BulletML parser.
 
-http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/index_e.html
+This is based on the format described at
+http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/bulletml_ref_e.html.
+
+Unless you are adding support for new tags, the only class you should
+care about in here is BulletML.
 """
 
 from __future__ import division
@@ -17,6 +21,8 @@ except ImportError:
 from bulletml.errors import Error
 from bulletml.expr import NumberDef, INumberDef
 
+__all_ = ["ParseError", "BulletML"]
+
 class ParseError(Error):
     """Raised when an error occurs parsing the XML structure."""
     pass
@@ -31,7 +37,7 @@ def realtag(element):
 class ParamList(object):
     """List of parameter definitions."""
 
-    def __init__(self, params=[]):
+    def __init__(self, params=()):
         self.params = list(params)
 
     @classmethod
@@ -312,7 +318,13 @@ class BulletRef(object):
             type(self).__name__, self.params, self.bullet)
 
 class ActionDef(object):
-    """Action definition."""
+    """Action definition.
+
+    To support parsing new actions, add tags to
+    ActionDef.CONSTRUCTORS. It maps tag names to classes with a
+    FromElement classmethod, which take the BulletML instance and
+    ElementTree element as arguments.
+    """
 
     # This is self-referential, so it's filled in later.
     CONSTRUCTORS = dict()
@@ -363,19 +375,51 @@ class ActionRef(object):
         return "%s(params=%r, action=%r)" % (
             type(self).__name__, self.params, self.action)
 
+class Offset(object):
+    """Provide an offset to a bullet's initial position."""
+
+    VALID_TYPES = ["relative", "absolute"]
+
+    def __init__(self, type, x, y):
+        if type not in self.VALID_TYPES:
+            raise ValueError("invalid type %r" % type)
+        self.type = type
+        self.x = x
+        self.y = y
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        type = element.get("type", "relative")
+        x = None
+        y = None
+        for subelem in element:
+            tag = realtag(subelem)
+            if tag == "x":
+                x = NumberDef(subelem.text)
+            elif tag == "y":
+                y = NumberDef(subelem.text)
+        return cls(type, x, y)
+
+    def __call__(self, params, rank):
+        return (self.x(params, rank) if self.x else 0,
+                self.y(params, rank) if self.y else 0)
+
 class FireDef(object):
     """Fire definition (creates a bullet)."""
 
-    def __init__(self, bullet, direction=None, speed=None):
+    def __init__(self, bullet, direction=None, speed=None, offset=None):
         self.bullet = bullet
         self.direction = direction
         self.speed = speed
+        self.offset = offset
 
     @classmethod
     def FromElement(cls, doc, element):
         """Construct using an ElementTree-style element."""
         direction = None
         speed = None
+        offset = None
 
         for subelem in element.getchildren():
             tag = realtag(subelem)
@@ -387,9 +431,10 @@ class FireDef(object):
                 bullet = BulletDef.FromElement(doc, subelem)
             elif tag == "bulletRef":
                 bullet = BulletRef.FromElement(doc, subelem)
-
+            elif tag == "offset":
+                offset = Offset.FromElement(doc, subelem)
         try:
-            fire = cls(bullet, direction, speed)
+            fire = cls(bullet, direction, speed, offset)
         except UnboundLocalError as exc:
             raise ParseError(str(exc))
         else:
@@ -402,7 +447,7 @@ class FireDef(object):
             direction = self.direction(params, rank)
         if self.speed:
             speed = self.speed(params, rank)
-        return direction, speed, actions
+        return direction, speed, actions, self.offset
 
     def __repr__(self):
         return "%s(direction=%r, speed=%r, bullet=%r)" % (
@@ -423,7 +468,6 @@ class FireRef(object):
         return fired
 
     def __call__(self, params, rank):
-        """Generate a Bullet from the FireDef and params."""
         return self.fire(self.params(params, rank), rank)
 
     def __repr__(self):
@@ -435,6 +479,12 @@ class BulletML(object):
 
     A BulletML document is a collection of bullets, actions, and
     firings, as well as a base game type.
+
+    You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
+    its parsing. It maps tag names to classes with a FromElement
+    classmethod, which take the BulletML instance and ElementTree
+    element as arguments.
+    
     """
 
     CONSTRUCTORS = dict(
@@ -443,22 +493,26 @@ class BulletML(object):
         fire=FireDef,
         )
 
-    def __init__(self, source):
-        self.bullets = {}
-        self.actions = {}
-        self.fires = {}
-
-        self._bullet_refs = []
-        self._action_refs = []
-        self._fire_refs = []
+    def __init__(self, type="none", bullets=None, fires=None, actions=None):
+        self.type = type
+        self.bullets = {} if bullets is None else bullets
+        self.actions = {} if actions is None else actions
+        self.fires = {} if fires is None else fires
 
+    @classmethod
+    def FromDocument(cls, source):
+        """Return a BulletML instance based on a string or file-like."""
         if isinstance(source, (str, unicode)):
             source = StringIO(source)
 
         tree = ElementTree()
         root = tree.parse(source)
 
-        self.type = root.get("type", "none")
+        self = cls(type=root.get("type", "none"))
+
+        self._bullet_refs = []
+        self._action_refs = []
+        self._fire_refs = []
 
         for element in root.getchildren():
             tag = realtag(element)
@@ -483,6 +537,8 @@ class BulletML(object):
         self.actions.pop(None, None)
         self.fires.pop(None, None)
 
+        return self
+
     @property
     def top(self):
         """Get a list of all top-level actions."""