Psyco-enable the test runner. Fix a copy/paste error.
[python-bulletml.git] / bulletml / impl.py
1 """BulletML implementation."""
2
3 from __future__ import division
4
5 import math
6
7 from bulletml import parser
8
9 # TODO(jfw): This is very non-Pythonic, it's pretty much just the
10 # BulletML reference ActionImpl translated to Python.
11
12 PI_2 = math.pi * 2
13
14 __all__ = ["Action", "Bullet"]
15
16 class Action(object):
17 """Running action implementation."""
18
19 def __init__(self, owner, parent, actions, params, rank, repeat=1):
20 self.actions = actions
21 self.parent = parent
22 self.repeat = repeat
23 self.wait_frames = 0
24 self.speed = 0
25 self.speed_frames = 0
26 self.direction = 0
27 self.direction_frames = 0
28 self.aiming = False
29 self.mx = 0
30 self.my = 0
31 self.accel_frames = 0
32 self.previous_fire_direction = 0
33 self.previous_fire_speed = 0
34 self.params = params
35 self.pc = -1
36 self.finished = False
37 if parent:
38 self.copy_state(parent)
39
40 def __repr__(self):
41 return "%s(pc=%r, actions=%r)" % (
42 type(self).__name__, self.pc, self.actions)
43
44 def vanish(self):
45 """End this action and its parents."""
46 if self.parent:
47 self.parent.vanish()
48 self.pc = None
49 self.finished = True
50
51 def copy_state(self, other):
52 """Copy fire/movement state from other to self."""
53 self.direction_frames = other.direction_frames
54 self.direction = other.direction
55 self.aiming = other.aiming
56 self.speed_frames = other.speed_frames
57 self.speed = other.speed
58 self.accel_frames = other.accel_frames
59 self.mx = other.mx
60 self.my = other.my
61 self.previous_fire_direction = other.previous_fire_direction
62 self.previous_fire_speed = other.previous_fire_speed
63
64 def step(self, owner, rank):
65 """Advance by one frame."""
66 created = []
67
68 if self.speed_frames > 0:
69 self.speed_frames -= 1
70 owner.speed += self.speed
71
72 if self.direction_frames > 0:
73 # The Noiz implementation was a little weird here, I think
74 # there was a bug in it that prevented it from working if
75 # the frame count was 1. I'm still not sure what the aim
76 # check is supposed to do, exactly.
77 self.direction_frames -= 1
78 if self.aiming and self.direction_frames <= 0:
79 owner.direction += owner.aim
80 else:
81 owner.direction += self.direction
82
83 if self.accel_frames > 0:
84 self.accel_frames -= 1
85 owner.mx += self.mx
86 owner.my += self.my
87
88 if self.pc is None:
89 return created
90
91 if self.wait_frames > 0:
92 self.wait_frames -= 1
93 return created
94
95 while True:
96 self.pc += 1
97
98 try:
99 action = self.actions[self.pc]
100 except IndexError:
101 self.repeat -= 1
102 if self.repeat <= 0:
103 self.pc = None
104 self.finished = True
105 if self.parent is not None:
106 self.parent.copy_state(self)
107 owner.replace(self, self.parent)
108 break
109 else:
110 self.pc = 0
111 action = self.actions[self.pc]
112
113 if isinstance(action, parser.Repeat):
114 repeat, (actions, params) = action(self.params, rank)
115 child = Action(owner, self, actions, params, rank, repeat)
116 owner.replace(self, child)
117 created.extend(child.step(owner, rank))
118 break
119
120 elif isinstance(action, (parser.ActionDef, parser.ActionRef)):
121 actions, params = action(self.params, rank)
122 child = Action(owner, self, actions, params, rank)
123 owner.replace(self, child)
124 created.extend(child.step(owner, rank))
125 break
126
127 elif isinstance(action, (parser.FireDef, parser.FireRef)):
128 direction, speed, actions, offset = action(self.params, rank)
129 if direction:
130 direction, type = direction
131 if type == "aim" or type is None:
132 direction += owner.aim
133 elif type == "sequence":
134 direction += self.previous_fire_direction
135 elif type == "relative":
136 direction += owner.direction
137 else:
138 direction = owner.aim
139 self.previous_fire_direction = direction
140
141 if speed:
142 speed, type = speed
143 if type == "sequence":
144 speed += self.previous_fire_speed
145 elif type == "relative":
146 # The reference Noiz implementation uses
147 # prvFireSpeed here, but the standard is
148 # pretty clear -- "In case of the type is
149 # "relative", ... the speed is relative to the
150 # speed of this bullet."
151 speed += owner.speed
152 else:
153 speed = 1
154 self.previous_fire_speed = speed
155
156 x, y = owner.x, owner.y
157 if offset:
158 off_x, off_y = offset(self.params, rank)
159 if offset.type == "relative":
160 sin = math.sin(direction)
161 cos = math.cos(direction)
162 x += cos * off_x + sin * off_y
163 y += sin * off_x - cos * off_y
164 else:
165 x += off_x
166 y += off_y
167
168 bullet = Bullet(
169 x, y, direction, speed, owner.target, actions, self, rank)
170 created.append(bullet)
171
172 elif isinstance(action, parser.ChangeSpeed):
173 frames, (speed, type) = action(self.params, rank)
174 self.speed_frames = frames
175 if type == "sequence":
176 self.speed = speed
177 elif type == "relative":
178 self.speed = speed / frames
179 else:
180 self.speed = (speed - owner.speed) / frames
181
182 elif isinstance(action, parser.ChangeDirection):
183 frames, (direction, type) = action(self.params, rank)
184 self.direction_frames = frames
185 self.aiming = False
186 if type == "sequence":
187 self.direction = direction
188 else:
189 if type == "absolute":
190 self.direction = (
191 direction - owner.direction) % PI_2
192 elif type == "relative":
193 self.direction = direction
194 else:
195 self.aiming = True
196 self.direction = (
197 direction
198 + owner.aim
199 - owner.direction) % PI_2
200
201 if self.direction > math.pi:
202 self.direction -= PI_2
203 if self.direction < -math.pi:
204 self.direction += PI_2
205 self.direction /= self.direction_frames
206
207 elif isinstance(action, parser.Accel):
208 frames, horizontal, vertical = action(self.params, rank)
209 self.accel_frames = frames
210 if horizontal:
211 mx, type = horizontal
212 if type == "sequence":
213 self.mx = mx
214 elif type == "absolute":
215 self.mx = (mx - owner.mx) / frames
216 elif type == "relative":
217 self.mx = mx / frames
218 if vertical:
219 my, type = vertical
220 if type == "sequence":
221 self.my = my
222 elif type == "absolute":
223 self.my = (my - owner.my) / frames
224 elif type == "relative":
225 self.my = my / frames
226
227 elif isinstance(action, parser.Tag):
228 owner.tags.add(action.tag)
229
230 elif isinstance(action, parser.Untag):
231 try:
232 owner.tags.remove(action.tag)
233 except KeyError:
234 pass
235
236 elif isinstance(action, parser.Wait):
237 self.wait_frames = action(self.params, rank)
238 break
239
240 elif isinstance(action, parser.Vanish):
241 owner.vanish()
242 break
243
244 return created
245
246 class Bullet(object):
247 """Simple bullet implementation.
248
249 Attributes:
250 x, y - current X/Y position
251 px, py - X/Y position prior to the last step
252 mx, my - X/Y axis-oriented speed modifier ("acceleration")
253 direction - direction of movement, in radians
254 speed - speed of movement, in units per frame
255 target - object with .x and .y fields for "aim" directions
256 vanished - set to true by a <vanish> action
257 rank - game difficulty, 0 to 1, default 0.5
258 tags - string tags set by the running actions
259
260 Contructor Arguments:
261 x, y, direction, speed, target, rank - same as the attributes
262 actions - internal action list
263 parent - parent of actions, None for manually-created bullets
264
265
266 """
267
268 def __init__(self, x=0, y=0, direction=0, speed=0, target=None,
269 actions=(), parent=None, rank=0.5):
270 self.x = self.px = x
271 self.y = self.py = y
272 self.mx = 0
273 self.my = 0
274 self.direction = direction
275 self.speed = speed
276 self.vanished = False
277 self.target = target
278 self.rank = rank
279 self.tags = set()
280 # New bullets reset the parent hierarchy.
281 self._actions = [Action(self, None, action, params, rank)
282 for action, params in actions]
283
284 def __repr__(self):
285 return ("%s(%r, %r, accel=%r, direction=%r, speed=%r, "
286 "actions=%r, target=%r, vanished=%r)") % (
287 type(self).__name__, self.x, self.y, (self.mx, self.my),
288 self.direction, self.speed, self._actions, self.target,
289 self.vanished)
290
291 @property
292 def aim(self):
293 """Angle to the target, in radians."""
294 if self.target is None:
295 return self.direction
296 else:
297 return math.atan2(self.target.x - self.x, self.y - self.target.y)
298
299 @property
300 def finished(self):
301 """Check if this bullet is finished running.
302
303 A bullet is finished when it has vanished, and all its
304 actions have finished.
305
306 If this is true, the bullet should be removed from the screen.
307 (You will probably want to cull it under other circumstances
308 as well).
309 """
310 if not self.vanished:
311 return False
312 for action in self._actions:
313 if not action.finished:
314 return False
315 return True
316
317 def vanish(self):
318 """Vanish this bullet and stop all actions."""
319 self.vanished = True
320 for action in self._actions:
321 action.vanish()
322 self._actions = []
323
324 def replace(self, old, new):
325 """Replace an active action with another.
326
327 This is mostly used by actions internally to queue children.
328 """
329 try:
330 idx = self._actions.index(old)
331 except ValueError:
332 pass
333 else:
334 self._actions[idx] = new
335
336 def step(self):
337 """Advance by one frame.
338
339 This updates the position and velocity, and may also set the
340 vanished flag.
341
342 It returns any new bullets this bullet spawned during this step.
343 """
344 created = []
345
346 for action in self._actions:
347 created.extend(action.step(self, self.rank))
348
349 self.px = self.x
350 self.py = self.y
351 self.x += self.mx + math.sin(self.direction) * self.speed
352 self.y += self.my - math.cos(self.direction) * self.speed
353
354 return created