Convert degrees to radians at expression evaluation; use radians for rotation internally.
[python-bulletml.git] / bulletml / parser.py
index 933be80..b39437a 100644 (file)
@@ -5,6 +5,8 @@ http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/index_e.html
 
 from __future__ import division
 
+import math
+
 from xml.etree.ElementTree import ElementTree
 
 try:
@@ -28,12 +30,15 @@ def realtag(element):
 
 class ParamList(object):
     """List of parameter definitions."""
-    def __init__(self, element):
-        self.params = []
-        if element:
-            for subelem in element:
-                if realtag(subelem) == "param":
-                    self.params.append(NumberDef(subelem.text))
+
+    def __init__(self, params=[]):
+        self.params = list(params)
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        return cls([NumberDef(subelem.text) for subelem in element
+                    if realtag(subelem) == "param"])
 
     def __call__(self, params, rank):
         return [param(params, rank) for param in self.params]
@@ -44,12 +49,21 @@ class ParamList(object):
 class Direction(object):
     """Raw direction value."""
 
-    def __init__(self, doc, element, type="absolute"):
-        self.type = element.get("type", type)
-        self.value = NumberDef(element.text)
+    VALID_TYPES = ["relative", "absolute", "aim", "sequence"]
+
+    def __init__(self, type, value):
+        if type not in self.VALID_TYPES:
+            raise ValueError("invalid type %r" % type)
+        self.type = type
+        self.value = value
+
+    @classmethod
+    def FromElement(cls, doc, element, default="absolute"):
+        """Construct using an ElementTree-style element."""
+        return cls(element.get("type", default), NumberDef(element.text))
 
     def __call__(self, params, rank):
-        return (self.value(params, rank), self.type)
+        return (math.radians(self.value(params, rank)), self.type)
 
     def __repr__(self):
         return "%s(%r, type=%r)" % (
@@ -58,17 +72,23 @@ class Direction(object):
 class ChangeDirection(object):
     """Direction change over time."""
 
-    def __init__(self, doc, element):
+    def __init__(self, term, direction):
+        self.term = term
+        self.direction = direction
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
         for subelem in element.getchildren():
             tag = realtag(subelem)
             if tag == "direction":
-                self.direction = Direction(doc, subelem)
+                direction = Direction.FromElement(doc, subelem)
             elif tag == "term":
-                self.term = INumberDef(subelem.text)
+                term = INumberDef(subelem.text)
         try:
-            self.term, self.direction
-        except AttributeError:
-            raise ParseError
+            return cls(term, direction)
+        except UnboundLocalError as exc:
+            raise ParseError(str(exc))
 
     def __call__(self, params, rank):
         return self.term(params, rank), self.direction(params, rank)
@@ -80,9 +100,18 @@ class ChangeDirection(object):
 class Speed(object):
     """Raw speed value."""
 
-    def __init__(self, doc, element, type="absolute"):
-        self.type = element.get("type", type)
-        self.value = NumberDef(element.text)
+    VALID_TYPES = ["relative", "absolute", "sequence"]
+
+    def __init__(self, type, value):
+        if type not in self.VALID_TYPES:
+            raise ValueError("invalid type %r" % type)
+        self.type = type
+        self.value = value
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        return cls(element.get("type", "absolute"), NumberDef(element.text))
 
     def __call__(self, params, rank):
         return (self.value(params, rank), self.type)
@@ -93,17 +122,23 @@ class Speed(object):
 class ChangeSpeed(object):
     """Speed change over time."""
 
-    def __init__(self, doc, element):
+    def __init__(self, term, speed):
+        self.term = term
+        self.speed = speed
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
         for subelem in element.getchildren():
             tag = realtag(subelem)
             if tag == "speed":
-                self.speed = Speed(doc, subelem)
+                speed = Speed.FromElement(doc, subelem)
             elif tag == "term":
-                self.term = INumberDef(subelem.text)
+                term = INumberDef(subelem.text)
         try:
-            self.term, self.speed
-        except AttributeError:
-            raise ParseError
+            return cls(term, speed)
+        except UnboundLocalError as exc:
+            raise ParseError(str(exc))
 
     def __call__(self, params, rank):
         return self.term(params, rank), self.speed(params, rank)
@@ -114,8 +149,14 @@ class ChangeSpeed(object):
 
 class Wait(object):
     """Wait for some frames."""
-    def __init__(self, doc, element):
-        self.frames = INumberDef(element.text)
+
+    def __init__(self, frames):
+        self.frames = frames
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        return cls(INumberDef(element.text))
 
     def __call__(self, params, rank):
         return self.frames(params, rank)
@@ -125,29 +166,40 @@ class Wait(object):
 
 class Vanish(object):
     """Make the owner disappear."""
-    def __init__(self, doc, element):
+
+    def __init__(self):
         pass
 
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        return cls()
+
     def __repr__(self):
         return "%s()" % (type(self).__name__)
 
 class Repeat(object):
     """Repeat an action definition."""
 
-    def __init__(self, doc, element):
+    def __init__(self, times, action):
+        self.times = times
+        self.action = action
+    
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
         for subelem in element.getchildren():
             tag = realtag(subelem)
             if tag == "times":
-                self.times = INumberDef(subelem.text)
+                times = INumberDef(subelem.text)
             elif tag == "action":
-                self.action = ActionDef(doc, subelem)
+                action = ActionDef.FromElement(doc, subelem)
             elif tag == "actionRef":
-                self.action = ActionRef(doc, subelem)
-
+                action = ActionRef.FromElement(doc, subelem)
         try:
-            self.times, self.action
-        except AttributeError:
-            raise ParseError
+            return cls(times, action)
+        except UnboundLocalError as exc:
+            raise ParseError(str(exc))
 
     def __call__(self, params, rank):
         return self.times(params, rank), self.action(params, rank)
@@ -161,18 +213,28 @@ class Accel(object):
     horizontal = None
     vertical = None
 
-    def __init__(self, doc, element):
+    def __init__(self, term, horizontal=None, vertical=None):
+        self.term = term
+        self.horizontal = horizontal
+        self.vertical = vertical
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        horizontal = None
+        vertical = None
+
         for subelem in element.getchildren():
             tag = realtag(subelem)
             if tag == "term":
-                self.term = INumberDef(subelem.text)
+                term = INumberDef(subelem.text)
             elif tag == "horizontal":
-                self.horizontal = Speed(doc, subelem)
+                horizontal = Speed.FromElement(doc, subelem)
             elif tag == "vertical":
-                self.vertical = Speed(doc, subelem)
+                vertical = Speed.FromElement(doc, subelem)
 
         try:
-            self.term
+            return cls(term, horizontal, vertical)
         except AttributeError:
             raise ParseError
 
@@ -192,19 +254,30 @@ class BulletDef(object):
     direction = None
     speed = None
 
-    def __init__(self, doc, element):
-        self.actions = []
-        doc.bullets[element.get("label")] = self
+    def __init__(self, actions=[], direction=None, speed=None):
+        self.direction = direction
+        self.speed = speed
+        self.actions = list(actions)
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        actions = []
+        speed = None
+        direction = None
         for subelem in element.getchildren():
             tag = realtag(subelem)
             if tag == "direction":
-                self.direction = Direction(doc, subelem)
+                direction = Direction.FromElement(doc, subelem)
             elif tag == "speed":
-                self.speed = Speed(doc, subelem)
+                speed = Speed.FromElement(doc, subelem)
             elif tag == "action":
-                self.actions.append(ActionDef(doc, subelem))
+                actions.append(ActionDef.FromElement(doc, subelem))
             elif tag == "actionRef":
-                self.actions.append(ActionRef(doc, subelem))
+                actions.append(ActionRef.FromElement(doc, subelem))
+        dfn = cls(actions, direction, speed)
+        doc.bullets[element.get("label")] = dfn
+        return dfn
 
     def __call__(self, params, rank):
         actions = [action(params, rank) for action in self.actions]
@@ -220,10 +293,16 @@ class BulletDef(object):
 class BulletRef(object):
     """Create a bullet by name with parameters."""
 
-    def __init__(self, doc, element):
-        self.bullet = element.get("label")
-        self.params = ParamList(element)
-        doc._bullet_refs.append(self)
+    def __init__(self, bullet, params=None):
+        self.bullet = bullet
+        self.params = params or ParamList()
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        bullet = cls(element.get("label"), ParamList.FromElement(doc, element))
+        doc._bullet_refs.append(bullet)
+        return bullet
 
     def __call__(self, params, rank):
         return self.bullet(self.params(params, rank), rank)
@@ -235,27 +314,27 @@ class BulletRef(object):
 class ActionDef(object):
     """Action definition."""
 
-    def __init__(self, doc, element):
-        doc.actions[element.get("label")] = self
-        self.actions = []
+    # This is self-referential, so it's filled in later.
+    CONSTRUCTORS = dict()
+
+    def __init__(self, actions):
+        self.actions = list(actions)
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        actions = []
         for subelem in element.getchildren():
             tag = realtag(subelem)
             try:
-                ctr = dict(
-                    repeat=Repeat,
-                    fire=FireDef,
-                    fireRef=FireRef,
-                    changeSpeed=ChangeSpeed,
-                    changeDirection=ChangeDirection,
-                    accel=Accel,
-                    wait=Wait,
-                    vanish=Vanish,
-                    action=ActionDef,
-                    actionRef=ActionRef)[tag]
+                ctr = cls.CONSTRUCTORS[tag]
             except KeyError:
                 continue
             else:
-                self.actions.append(ctr(doc, subelem))
+                actions.append(ctr.FromElement(doc, subelem))
+        dfn = cls(actions)
+        doc.actions[element.get("label")] = dfn
+        return dfn
 
     def __call__(self, params, rank):
         return self.actions, params
@@ -266,10 +345,16 @@ class ActionDef(object):
 class ActionRef(object):
     """Run an action by name with parameters."""
 
-    def __init__(self, doc, element):
-        self.action = element.get("label")
-        self.params = ParamList(element)
-        doc._action_refs.append(self)
+    def __init__(self, action, params=None):
+        self.action = action
+        self.params = params or ParamList()
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        action = cls(element.get("label"), ParamList.FromElement(doc, element))
+        doc._action_refs.append(action)
+        return action
 
     def __call__(self, params, rank):
         return self.action(self.params(params, rank), rank)
@@ -281,25 +366,35 @@ class ActionRef(object):
 class FireDef(object):
     """Fire definition (creates a bullet)."""
 
-    direction = None
-    speed = None
+    def __init__(self, bullet, direction=None, speed=None):
+        self.bullet = bullet
+        self.direction = direction
+        self.speed = speed
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        direction = None
+        speed = None
 
-    def __init__(self, doc, element):
-        doc.fires[element.get("label")] = self
         for subelem in element.getchildren():
             tag = realtag(subelem)
             if tag == "direction":
-                self.direction = Direction(doc, subelem, "aim")
+                direction = Direction.FromElement(doc, subelem, "aim")
             elif tag == "speed":
-                self.speed = Speed(doc, subelem)
+                speed = Speed.FromElement(doc, subelem)
             elif tag == "bullet":
-                self.bullet = BulletDef(doc, subelem)
+                bullet = BulletDef.FromElement(doc, subelem)
             elif tag == "bulletRef":
-                self.bullet = BulletRef(doc, subelem)
+                bullet = BulletRef.FromElement(doc, subelem)
+
         try:
-            self.bullet
-        except AttributeError:
-            raise ParseError
+            fire = cls(bullet, direction, speed)
+        except UnboundLocalError as exc:
+            raise ParseError(str(exc))
+        else:
+            doc.fires[element.get("label")] = fire
+            return fire
 
     def __call__(self, params, rank):
         direction, speed, actions = self.bullet(params, rank)
@@ -316,10 +411,16 @@ class FireDef(object):
 class FireRef(object):
     """Fire a bullet by name with parameters."""
 
-    def __init__(self, doc, element):
-        self.fire = element.get("label")
-        self.params = ParamList(element)
-        doc._fire_refs.append(self)
+    def __init__(self, fire, params=None):
+        self.fire = fire
+        self.params = params or ParamList()
+
+    @classmethod
+    def FromElement(cls, doc, element):
+        """Construct using an ElementTree-style element."""
+        fired = cls(element.get("label"), ParamList.FromElement(doc, element))
+        doc._fire_refs.append(fired)
+        return fired
 
     def __call__(self, params, rank):
         """Generate a Bullet from the FireDef and params."""
@@ -362,7 +463,7 @@ class BulletML(object):
         for element in root.getchildren():
             tag = realtag(element)
             if tag in self.CONSTRUCTORS:
-                self.CONSTRUCTORS[tag](self, element)
+                self.CONSTRUCTORS[tag].FromElement(self, element)
 
         try:
             for ref in self._bullet_refs:
@@ -378,6 +479,10 @@ class BulletML(object):
         del(self._action_refs)
         del(self._fire_refs)
 
+        self.bullets.pop(None, None)
+        self.actions.pop(None, None)
+        self.fires.pop(None, None)
+
     @property
     def top(self):
         """Get a list of all top-level actions."""
@@ -388,3 +493,15 @@ class BulletML(object):
         return "%s(type=%r, bullets=%r, actions=%r, fires=%r)" % (
             type(self).__name__, self.type, self.bullets, self.actions,
             self.fires)
+
+ActionDef.CONSTRUCTORS = dict(
+        repeat=Repeat,
+        fire=FireDef,
+        fireRef=FireRef,
+        changeSpeed=ChangeSpeed,
+        changeDirection=ChangeDirection,
+        accel=Accel,
+        wait=Wait,
+        vanish=Vanish,
+        action=ActionDef,
+        actionRef=ActionRef)