Major refactor. Since Python is duck-typed the parser/impl split in the Java BulletML...
[python-bulletml.git] / bulletml / impl.py
index 98b0ef7..ca800be 100644 (file)
@@ -1,21 +1,27 @@
-"""BulletML implementation.
-
-http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/index_e.html
-"""
+"""BulletML implementation."""
 
 from __future__ import division
 
 
 from __future__ import division
 
-import math
+from math import atan2, sin, cos
 
 from bulletml import parser
 
 
 from bulletml import parser
 
-# TODO(jfw): This is very non-Pythonic, it's pretty much just the
-# BulletML reference ActionImpl translated to Python.
-
-PI_2 = math.pi * 2
+__all__ = ["Action", "Bullet"]
 
 class Action(object):
 
 class Action(object):
-    """Running action implementation."""
+    """Running action implementation.
+
+    To implement new actions, add a new element/class pair to
+    parser.ActionDef.CONSTRUCTORS.  It should support FromXML,
+    __getstate__, and __setstate__, and 5-ary __call__:
+
+        def __call__(self, owner, action, params, rank, created)
+
+    Which will be called to execute it. This function should modify
+    owner, action, and created in-place, and return true if action
+    execution should stop for this bullet this frame.
+
+    """
 
     def __init__(self, owner, parent, actions, params, rank, repeat=1):
         self.actions = actions
 
     def __init__(self, owner, parent, actions, params, rank, repeat=1):
         self.actions = actions
@@ -29,12 +35,10 @@ class Action(object):
         self.aiming = False
         self.mx = 0
         self.my = 0
         self.aiming = False
         self.mx = 0
         self.my = 0
-        self.owner = owner
         self.accel_frames = 0
         self.previous_fire_direction = 0
         self.previous_fire_speed = 0
         self.params = params
         self.accel_frames = 0
         self.previous_fire_direction = 0
         self.previous_fire_speed = 0
         self.params = params
-        self.rank = rank
         self.pc = -1
         self.finished = False
         if parent:
         self.pc = -1
         self.finished = False
         if parent:
@@ -64,36 +68,34 @@ class Action(object):
         self.previous_fire_direction = other.previous_fire_direction
         self.previous_fire_speed = other.previous_fire_speed
 
         self.previous_fire_direction = other.previous_fire_direction
         self.previous_fire_speed = other.previous_fire_speed
 
-    def step(self):
+    def step(self, owner, created):
         """Advance by one frame."""
         """Advance by one frame."""
-        created = []
+        s_params = self.params
+        rank = owner.rank
 
         if self.speed_frames > 0:
             self.speed_frames -= 1
 
         if self.speed_frames > 0:
             self.speed_frames -= 1
-            self.owner.speed += self.speed
+            owner.speed += self.speed
 
         if self.direction_frames > 0:
 
         if self.direction_frames > 0:
-            # The Noiz implementation was a little weird here, I think
-            # there was a bug in it that prevented it from working if
-            # the frame count was 1. I'm still not sure what the aim
-            # check is supposed to do, exactly.
+            # I'm still not sure what the aim check is supposed to do.
             self.direction_frames -= 1
             if self.aiming and self.direction_frames <= 0:
             self.direction_frames -= 1
             if self.aiming and self.direction_frames <= 0:
-                self.owner.direction += self.owner.aim
+                owner.direction += owner.aim
             else:
             else:
-                self.owner.direction += self.direction
+                owner.direction += self.direction
 
         if self.accel_frames > 0:
             self.accel_frames -= 1
 
         if self.accel_frames > 0:
             self.accel_frames -= 1
-            self.owner.mx += self.mx
-            self.owner.my += self.my
+            owner.mx += self.mx
+            owner.my += self.my
 
         if self.pc is None:
 
         if self.pc is None:
-            return created
+            return
 
         if self.wait_frames > 0:
             self.wait_frames -= 1
 
         if self.wait_frames > 0:
             self.wait_frames -= 1
-            return created
+            return
 
         while True:
             self.pc += 1
 
         while True:
             self.pc += 1
@@ -107,130 +109,48 @@ class Action(object):
                     self.finished = True
                     if self.parent is not None:
                         self.parent.copy_state(self)
                     self.finished = True
                     if self.parent is not None:
                         self.parent.copy_state(self)
-                        self.owner.replace(self, self.parent)
+                        owner.replace(self, self.parent)
                     break
                 else:
                     self.pc = 0
                     action = self.actions[self.pc]
 
                     break
                 else:
                     self.pc = 0
                     action = self.actions[self.pc]
 
-            if isinstance(action, parser.Repeat):
-                repeat, (actions, params) = action(self.params, self.rank)
-                child = Action(
-                    self.owner, self, actions, params, self.rank, repeat)
-                self.owner.replace(self, child)
-                created.extend(child.step())
-                break
-
-            elif isinstance(action, (parser.ActionDef, parser.ActionRef)):
-                actions, params = action(self.params, self.rank)
-                child = Action(self.owner, self, actions, params, self.rank)
-                self.owner.replace(self, child)
-                created.extend(child.step())
-                break
-
-            elif isinstance(action, (parser.FireDef, parser.FireRef)):
-                direction, speed, actions = action(self.params, self.rank)
-                if direction:
-                    direction, type = direction
-                    if type == "aim" or type is None:
-                        direction += self.owner.aim
-                    elif type == "sequence":
-                        direction += self.previous_fire_direction
-                    elif type == "relative":
-                        direction += self.owner.direction
-                else:
-                    direction = self.owner.aim
-                self.previous_fire_direction = direction
-
-                if speed:
-                    speed, type = speed
-                    if type == "sequence":
-                        speed += self.previous_fire_speed
-                    elif type == "relative":
-                        # The reference Noiz implementation uses
-                        # prvFireSpeed here, but the standard is
-                        # pretty clear -- "0 means that the direction
-                        # of this fire and the direction of the bullet
-                        # are the same".
-                        speed += self.owner.speed
-                else:
-                    speed = 1
-                self.previous_fire_speed = speed
-
-                bullet = Bullet(self.owner.x, self.owner.y, direction, speed,
-                                self.owner.target, actions, self)
-                created.append(bullet)
-
-            elif isinstance(action, parser.ChangeSpeed):
-                frames, (speed, type) = action(self.params, self.rank)
-                self.speed_frames = frames
-                if type == "sequence":
-                    self.speed = speed
-                elif type == "relative":
-                    self.speed = speed / frames
-                else:
-                    self.speed = (speed - self.owner.speed) / frames
-
-            elif isinstance(action, parser.ChangeDirection):
-                frames, (direction, type) = action(self.params, self.rank)
-                self.direction_frames = frames
-                self.aiming = False
-                if type == "sequence":
-                    self.direction = direction
-                else:
-                    if type == "absolute":
-                        self.direction = (
-                            direction - self.owner.direction) % PI_2
-                    elif type == "relative":
-                        self.direction = direction
-                    else:
-                        self.aiming = True
-                        self.direction = (
-                            direction
-                            + self.owner.aim
-                            - self.owner.direction) % PI_2
-
-                    if self.direction > math.pi:
-                        self.direction -= PI_2
-                    if self.direction < -math.pi:
-                        self.direction += PI_2
-                    self.direction /= self.direction_frames
-
-            elif isinstance(action, parser.Accel):
-                frames, horizontal, vertical = action(self.params, self.rank)
-                self.accel_frames = frames
-                if horizontal:
-                    mx, type = horizontal
-                    if type == "sequence":
-                        self.mx = mx
-                    elif type == "absolute":
-                        self.mx = (mx - self.owner.mx) / frames
-                    elif type == "relative":
-                        self.mx = mx / frames
-                if vertical:
-                    my, type = vertical
-                    if type == "sequence":
-                        self.my = my
-                    elif type == "absolute":
-                        self.my = (my - self.owner.my) / frames
-                    elif type == "relative":
-                        self.my = my / frames
-
-            elif isinstance(action, parser.Wait):
-                self.wait_frames = action(self.params, self.rank)
+            if isinstance(action, (parser.ActionDef, parser.ActionRef)):
+                actions, params = action(s_params, rank)
+                child = self.__class__(owner, self, actions, params, rank)
+                owner.replace(self, child)
+                child.step(owner, created)
                 break
 
                 break
 
-            elif isinstance(action, parser.Vanish):
-                self.owner.vanish()
+            elif action(owner, self, s_params, rank, created):
                 break
 
                 break
 
-        return created
-
 class Bullet(object):
 class Bullet(object):
-    """Simple bullet implementation."""
+    """Simple bullet implementation.
+
+    Attributes:
+    x, y - current X/Y position
+    px, py - X/Y position prior to the last step
+    mx, my - X/Y axis-oriented speed modifier ("acceleration")
+    direction - direction of movement, in radians
+    speed - speed of movement, in units per frame
+    target - object with .x and .y fields for "aim" directions
+    vanished - set to true by a <vanish> action
+    rank - game difficulty, 0 to 1, default 0.5
+    tags - string tags set by the running actions
+    appearance - string used to set bullet appearance
+
+    Contructor Arguments:
+    x, y, direction, speed, target, rank, tags, appearance
+        - same as the above attributes
+    actions - internal action list
+    Action - custom Action constructor
+
+    """
 
     def __init__(self, x=0, y=0, direction=0, speed=0, target=None,
 
     def __init__(self, x=0, y=0, direction=0, speed=0, target=None,
-                 actions=(), parent=None, rank=None):
+                 actions=(), rank=0.5, tags=(), appearance=None,
+                 Action=Action):
         self.x = self.px = x
         self.y = self.py = y
         self.mx = 0
         self.x = self.px = x
         self.y = self.py = y
         self.mx = 0
@@ -239,31 +159,49 @@ class Bullet(object):
         self.speed = speed
         self.vanished = False
         self.target = target
         self.speed = speed
         self.vanished = False
         self.target = target
-        if rank is None:
-            rank = parent.rank if parent else 0.5
+        self.rank = rank
+        self.tags = set(tags)
+        self.appearance = appearance
         # New bullets reset the parent hierarchy.
         self._actions = [Action(self, None, action, params, rank)
                          for action, params in actions]
 
         # New bullets reset the parent hierarchy.
         self._actions = [Action(self, None, action, params, rank)
                          for action, params in actions]
 
+    @classmethod
+    def FromDocument(cls, doc, x=0, y=0, direction=0, speed=0, target=None,
+                     params=(), rank=0.5, Action=Action):
+        """Construct a new Bullet from a loaded BulletML document."""
+        actions = [a(params, rank) for a in doc.actions]
+        return cls(x=x, y=y, direction=direction, speed=speed,
+                   target=target, actions=actions, rank=rank, Action=Action)
+
     def __repr__(self):
         return ("%s(%r, %r, accel=%r, direction=%r, speed=%r, "
     def __repr__(self):
         return ("%s(%r, %r, accel=%r, direction=%r, speed=%r, "
-                "actions=%r, target=%r, vanished=%r)") % (
+                "actions=%r, target=%r, appearance=vanished=%r)") % (
             type(self).__name__, self.x, self.y, (self.mx, self.my),
             self.direction, self.speed, self._actions, self.target,
             type(self).__name__, self.x, self.y, (self.mx, self.my),
             self.direction, self.speed, self._actions, self.target,
-            self.vanished)
+            self.appearance, self.vanished)
 
     @property
     def aim(self):
 
     @property
     def aim(self):
-        """Angle to the target."""
-        if self.target is None:
-            return self.direction
+        """Angle to the target, in radians.
+
+        If the target does not exist or cannot be found, return 0.
+        """
+        try:
+            target_x = self.target.x
+            target_y = self.target.y
+        except AttributeError:
+            return 0
         else:
         else:
-            return math.atan2(self.target.x - self.x, self.y - self.target.y)
+            return atan2(target_x - self.x, target_y - self.y)
 
     @property
     def finished(self):
         """Check if this bullet is finished running.
 
 
     @property
     def finished(self):
         """Check if this bullet is finished running.
 
+        A bullet is finished when it has vanished, and all its
+        actions have finished.
+
         If this is true, the bullet should be removed from the screen.
         (You will probably want to cull it under other circumstances
         as well).
         If this is true, the bullet should be removed from the screen.
         (You will probably want to cull it under other circumstances
         as well).
@@ -283,7 +221,10 @@ class Bullet(object):
         self._actions = []
 
     def replace(self, old, new):
         self._actions = []
 
     def replace(self, old, new):
-        """Replace an active action with another."""
+        """Replace an active action with another.
+
+        This is mostly used by actions internally to queue children.
+        """
         try:
             idx = self._actions.index(old)
         except ValueError:
         try:
             idx = self._actions.index(old)
         except ValueError:
@@ -294,17 +235,19 @@ class Bullet(object):
     def step(self):
         """Advance by one frame.
 
     def step(self):
         """Advance by one frame.
 
-        This updates the direction, speed, x, y, px, and py members,
-        and may set the vanished flag.
+        This updates the position and velocity, and may also set the
+        vanished flag.
+
+        It returns any new bullets this bullet spawned during this step.
         """
         created = []
 
         for action in self._actions:
         """
         created = []
 
         for action in self._actions:
-            created.extend(action.step())
+            action.step(self, created)
 
         self.px = self.x
         self.py = self.y
 
         self.px = self.x
         self.py = self.y
-        self.x += self.mx + math.sin(self.direction) * self.speed
-        self.y += self.my - math.cos(self.direction) * self.speed
+        self.x += self.mx + sin(self.direction) * self.speed
+        self.y += -self.my + cos(self.direction) * self.speed
 
         return created
 
         return created