Python 3 support.
[python-bulletml.git] / bulletml / impl.py
index a056d6c..0548b9d 100644 (file)
@@ -1,7 +1,4 @@
-"""BulletML implementation.
-
-http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/index_e.html
-"""
+"""BulletML implementation."""
 
 from __future__ import division
 
 
 from __future__ import division
 
@@ -14,6 +11,8 @@ from bulletml import parser
 
 PI_2 = math.pi * 2
 
 
 PI_2 = math.pi * 2
 
+__all__ = ["Action", "Bullet"]
+
 class Action(object):
     """Running action implementation."""
 
 class Action(object):
     """Running action implementation."""
 
@@ -29,12 +28,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,33 +61,36 @@ 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, rank, created, sin=math.sin, cos=math.cos):
         """Advance by one frame."""
         """Advance by one frame."""
-        created = []
+        s_params = self.params
 
         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.
             self.direction_frames -= 1
             self.direction_frames -= 1
-            if self.direction_frames <= 0:
-                if self.aiming:
-                    self.owner.direction += self.owner.aim
+            if self.aiming and self.direction_frames <= 0:
+                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
@@ -104,39 +104,38 @@ 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]
 
             if isinstance(action, parser.Repeat):
                     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())
+                repeat, (actions, params) = action(s_params, rank)
+                child = Action(owner, self, actions, params, rank, repeat)
+                owner.replace(self, child)
+                child.step(owner, rank, created, sin, cos)
                 break
 
             elif isinstance(action, (parser.ActionDef, parser.ActionRef)):
                 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())
+                actions, params = action(s_params, rank)
+                child = Action(owner, self, actions, params, rank)
+                owner.replace(self, child)
+                child.step(owner, rank, created, sin, cos)
                 break
 
             elif isinstance(action, (parser.FireDef, parser.FireRef)):
                 break
 
             elif isinstance(action, (parser.FireDef, parser.FireRef)):
-                direction, speed, actions = action(self.params, self.rank)
+                direction, speed, actions, offset = action(s_params, rank)
                 if direction:
                     direction, type = direction
                     if type == "aim" or type is None:
                 if direction:
                     direction, type = direction
                     if type == "aim" or type is None:
-                        direction += self.owner.aim
+                        direction += owner.aim
                     elif type == "sequence":
                         direction += self.previous_fire_direction
                     elif type == "relative":
                     elif type == "sequence":
                         direction += self.previous_fire_direction
                     elif type == "relative":
-                        direction += self.owner.direction
+                        direction += owner.direction
                 else:
                 else:
-                    direction = self.owner.aim
+                    direction = owner.aim
                 self.previous_fire_direction = direction
 
                 if speed:
                 self.previous_fire_direction = direction
 
                 if speed:
@@ -146,88 +145,136 @@ class Action(object):
                     elif type == "relative":
                         # The reference Noiz implementation uses
                         # prvFireSpeed here, but the standard is
                     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
+                        # pretty clear -- "In case of the type is
+                        # "relative", ... the speed is relative to the
+                        # speed of this bullet."
+                        speed += owner.speed
                 else:
                     speed = 1
                 self.previous_fire_speed = speed
 
                 else:
                     speed = 1
                 self.previous_fire_speed = speed
 
-                bullet = Bullet(self.owner.x, self.owner.y, direction, speed,
-                                self.owner.target, actions, self)
+                x, y = owner.x, owner.y
+                if offset:
+                    off_x, off_y = offset(s_params, rank)
+                    if offset.type == "relative":
+                        s = sin(direction)
+                        c = cos(direction)
+                        x += c * off_x + s * off_y
+                        y += s * off_x - c * off_y
+                    else:
+                        x += off_x
+                        y += off_y
+
+                bullet = Bullet(
+                    x, y, direction, speed, owner.target, actions, self, rank)
                 created.append(bullet)
 
             elif isinstance(action, parser.ChangeSpeed):
                 created.append(bullet)
 
             elif isinstance(action, parser.ChangeSpeed):
-                frames, (speed, type) = action(self.params, self.rank)
+                frames, (speed, type) = action(s_params, rank)
                 self.speed_frames = frames
                 self.speed_frames = frames
-                if type == "sequence":
+                if frames <= 0:
+                    if type == "absolute":
+                        owner.speed = speed
+                    elif type == "relative":
+                        owner.speed += speed
+                elif type == "sequence":
                     self.speed = speed
                 elif type == "relative":
                     self.speed = speed / frames
                 else:
                     self.speed = speed
                 elif type == "relative":
                     self.speed = speed / frames
                 else:
-                    self.speed = (speed - self.owner.speed) / frames
+                    self.speed = (speed - owner.speed) / frames
 
             elif isinstance(action, parser.ChangeDirection):
 
             elif isinstance(action, parser.ChangeDirection):
-                frames, (direction, type) = action(self.params, self.rank)
+                frames, (direction, type) = action(s_params, rank)
                 self.direction_frames = frames
                 self.aiming = False
                 if type == "sequence":
                     self.direction = direction
                 else:
                     if type == "absolute":
                 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:
+                        direction -= owner.direction
+                    elif type != "relative": # aim or default
                         self.aiming = True
                         self.aiming = True
-                        self.direction = (
-                            direction
-                            + self.owner.aim
-                            - self.owner.direction) % PI_2
+                        direction += owner.aim - owner.direction
 
 
-                    if self.direction > math.pi:
-                        self.direction -= PI_2
-                    if self.direction < -math.pi:
-                        self.direction += PI_2
-                    self.direction /= self.direction_frames
+                    # Normalize to [-pi, pi).
+                    direction = (direction + math.pi) % PI_2 - math.pi
+                    if frames <= 0:
+                        owner.direction += direction
+                    else:
+                        self.direction = direction / frames
 
             elif isinstance(action, parser.Accel):
 
             elif isinstance(action, parser.Accel):
-                frames, horizontal, vertical = action(self.params, self.rank)
+                frames, horizontal, vertical = action(s_params, rank)
                 self.accel_frames = frames
                 if horizontal:
                     mx, type = horizontal
                 self.accel_frames = frames
                 if horizontal:
                     mx, type = horizontal
-                    if type == "sequence":
+                    if frames <= 0:
+                        if type == "absolute":
+                            owner.mx = mx
+                        elif type == "relative":
+                            owner.mx += mx
+                    elif type == "sequence":
                         self.mx = mx
                     elif type == "absolute":
                         self.mx = mx
                     elif type == "absolute":
-                        self.mx = (mx - self.owner.mx) / frames
+                        self.mx = (mx - owner.mx) / frames
                     elif type == "relative":
                         self.mx = mx / frames
                 if vertical:
                     my, type = vertical
                     elif type == "relative":
                         self.mx = mx / frames
                 if vertical:
                     my, type = vertical
-                    if type == "sequence":
+                    if frames <= 0:
+                        if type == "absolute":
+                            owner.my = my
+                        elif type == "relative":
+                            owner.my += my
+                    elif type == "sequence":
                         self.my = my
                     elif type == "absolute":
                         self.my = my
                     elif type == "absolute":
-                        self.my = (my - self.owner.my) / frames
+                        self.my = (my - owner.my) / frames
                     elif type == "relative":
                         self.my = my / frames
 
                     elif type == "relative":
                         self.my = my / frames
 
+            elif isinstance(action, parser.Tag):
+                owner.tags.add(action.tag)
+
+            elif isinstance(action, parser.Untag):
+                try:
+                    owner.tags.remove(action.tag)
+                except KeyError:
+                    pass
+
             elif isinstance(action, parser.Wait):
             elif isinstance(action, parser.Wait):
-                self.wait_frames = action(self.params, self.rank)
+                self.wait_frames = action(s_params, rank)
                 break
 
             elif isinstance(action, parser.Vanish):
                 break
 
             elif isinstance(action, parser.Vanish):
-                self.owner.vanish()
+                owner.vanish()
                 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
+
+    Contructor Arguments:
+    x, y, direction, speed, target, rank - same as the attributes
+    actions - internal action list
+    parent - parent of actions, None for manually-created bullets
+
+
+    """
 
     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=(), parent=None, rank=0.5):
         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
@@ -236,8 +283,8 @@ 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()
         # 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]
@@ -251,7 +298,7 @@ class Bullet(object):
 
     @property
     def aim(self):
 
     @property
     def aim(self):
-        """Angle to the target."""
+        """Angle to the target, in radians."""
         if self.target is None:
             return self.direction
         else:
         if self.target is None:
             return self.direction
         else:
@@ -261,6 +308,9 @@ class Bullet(object):
     def finished(self):
         """Check if this bullet is finished running.
 
     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).
@@ -280,7 +330,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:
@@ -288,27 +341,22 @@ class Bullet(object):
         else:
             self._actions[idx] = new
 
         else:
             self._actions[idx] = new
 
-    def step(self):
+    def step(self, sin=math.sin, cos=math.cos):
         """Advance by one frame.
 
         """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, self.rank, created, sin, cos)
 
         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
-
-    @classmethod
-    def FromDoc(cls, doc, params=(), x=0, y=0, speed=0, direction=0,
-                target=None, rank=0.5):
-        """Construct a bullet from top-level actions in a document."""
-        actions = [act(params, rank) for act in doc.top]
-        return cls(x, y, direction, speed, target, actions, rank=rank)