Fire, relative: I'm certain the reference implementation is wrong.
[python-bulletml.git] / bulletml / parser.py
1 """BulletML parser.
2
3 http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/index_e.html
4 """
5
6 from __future__ import division
7
8 import math
9
10 from xml.etree.ElementTree import ElementTree
11
12 try:
13 from cStringIO import StringIO
14 except ImportError:
15 from StringIO import StringIO
16
17 from bulletml.errors import Error
18 from bulletml.expr import NumberDef, INumberDef
19
20 class ParseError(Error):
21 """Raised when an error occurs parsing the XML structure."""
22 pass
23
24 def realtag(element):
25 """Strip namespace poop off the front of a tag."""
26 try:
27 return element.tag.rsplit('}', 1)[1]
28 except ValueError:
29 return element.tag
30
31 class ParamList(object):
32 """List of parameter definitions."""
33
34 def __init__(self, params=[]):
35 self.params = list(params)
36
37 @classmethod
38 def FromElement(cls, doc, element):
39 """Construct using an ElementTree-style element."""
40 return cls([NumberDef(subelem.text) for subelem in element
41 if realtag(subelem) == "param"])
42
43 def __call__(self, params, rank):
44 return [param(params, rank) for param in self.params]
45
46 def __repr__(self):
47 return "%s(%r)" % (type(self).__name__, self.params)
48
49 class Direction(object):
50 """Raw direction value."""
51
52 VALID_TYPES = ["relative", "absolute", "aim", "sequence"]
53
54 def __init__(self, type, value):
55 if type not in self.VALID_TYPES:
56 raise ValueError("invalid type %r" % type)
57 self.type = type
58 self.value = value
59
60 @classmethod
61 def FromElement(cls, doc, element, default="absolute"):
62 """Construct using an ElementTree-style element."""
63 return cls(element.get("type", default), NumberDef(element.text))
64
65 def __call__(self, params, rank):
66 return (math.radians(self.value(params, rank)), self.type)
67
68 def __repr__(self):
69 return "%s(%r, type=%r)" % (
70 type(self).__name__, self.value, self.type)
71
72 class ChangeDirection(object):
73 """Direction change over time."""
74
75 def __init__(self, term, direction):
76 self.term = term
77 self.direction = direction
78
79 @classmethod
80 def FromElement(cls, doc, element):
81 """Construct using an ElementTree-style element."""
82 for subelem in element.getchildren():
83 tag = realtag(subelem)
84 if tag == "direction":
85 direction = Direction.FromElement(doc, subelem)
86 elif tag == "term":
87 term = INumberDef(subelem.text)
88 try:
89 return cls(term, direction)
90 except UnboundLocalError as exc:
91 raise ParseError(str(exc))
92
93 def __call__(self, params, rank):
94 return self.term(params, rank), self.direction(params, rank)
95
96 def __repr__(self):
97 return "%s(term=%r, direction=%r)" % (
98 type(self).__name__, self.term, self.direction)
99
100 class Speed(object):
101 """Raw speed value."""
102
103 VALID_TYPES = ["relative", "absolute", "sequence"]
104
105 def __init__(self, type, value):
106 if type not in self.VALID_TYPES:
107 raise ValueError("invalid type %r" % type)
108 self.type = type
109 self.value = value
110
111 @classmethod
112 def FromElement(cls, doc, element):
113 """Construct using an ElementTree-style element."""
114 return cls(element.get("type", "absolute"), NumberDef(element.text))
115
116 def __call__(self, params, rank):
117 return (self.value(params, rank), self.type)
118
119 def __repr__(self):
120 return "%s(%r, type=%r)" % (type(self).__name__, self.value, self.type)
121
122 class ChangeSpeed(object):
123 """Speed change over time."""
124
125 def __init__(self, term, speed):
126 self.term = term
127 self.speed = speed
128
129 @classmethod
130 def FromElement(cls, doc, element):
131 """Construct using an ElementTree-style element."""
132 for subelem in element.getchildren():
133 tag = realtag(subelem)
134 if tag == "speed":
135 speed = Speed.FromElement(doc, subelem)
136 elif tag == "term":
137 term = INumberDef(subelem.text)
138 try:
139 return cls(term, speed)
140 except UnboundLocalError as exc:
141 raise ParseError(str(exc))
142
143 def __call__(self, params, rank):
144 return self.term(params, rank), self.speed(params, rank)
145
146 def __repr__(self):
147 return "%s(term=%r, speed=%r)" % (
148 type(self).__name__, self.term, self.speed)
149
150 class Wait(object):
151 """Wait for some frames."""
152
153 def __init__(self, frames):
154 self.frames = frames
155
156 @classmethod
157 def FromElement(cls, doc, element):
158 """Construct using an ElementTree-style element."""
159 return cls(INumberDef(element.text))
160
161 def __call__(self, params, rank):
162 return self.frames(params, rank)
163
164 def __repr__(self):
165 return "%s(%r)" % (type(self).__name__, self.frames)
166
167 class Vanish(object):
168 """Make the owner disappear."""
169
170 def __init__(self):
171 pass
172
173 @classmethod
174 def FromElement(cls, doc, element):
175 """Construct using an ElementTree-style element."""
176 return cls()
177
178 def __repr__(self):
179 return "%s()" % (type(self).__name__)
180
181 class Repeat(object):
182 """Repeat an action definition."""
183
184 def __init__(self, times, action):
185 self.times = times
186 self.action = action
187
188 @classmethod
189 def FromElement(cls, doc, element):
190 """Construct using an ElementTree-style element."""
191 for subelem in element.getchildren():
192 tag = realtag(subelem)
193 if tag == "times":
194 times = INumberDef(subelem.text)
195 elif tag == "action":
196 action = ActionDef.FromElement(doc, subelem)
197 elif tag == "actionRef":
198 action = ActionRef.FromElement(doc, subelem)
199 try:
200 return cls(times, action)
201 except UnboundLocalError as exc:
202 raise ParseError(str(exc))
203
204 def __call__(self, params, rank):
205 return self.times(params, rank), self.action(params, rank)
206
207 def __repr__(self):
208 return "%s(%r, %r)" % (type(self).__name__, self.times, self.action)
209
210 class Accel(object):
211 """Accelerate over some time."""
212
213 horizontal = None
214 vertical = None
215
216 def __init__(self, term, horizontal=None, vertical=None):
217 self.term = term
218 self.horizontal = horizontal
219 self.vertical = vertical
220
221 @classmethod
222 def FromElement(cls, doc, element):
223 """Construct using an ElementTree-style element."""
224 horizontal = None
225 vertical = None
226
227 for subelem in element.getchildren():
228 tag = realtag(subelem)
229 if tag == "term":
230 term = INumberDef(subelem.text)
231 elif tag == "horizontal":
232 horizontal = Speed.FromElement(doc, subelem)
233 elif tag == "vertical":
234 vertical = Speed.FromElement(doc, subelem)
235
236 try:
237 return cls(term, horizontal, vertical)
238 except AttributeError:
239 raise ParseError
240
241 def __call__(self, params, rank):
242 frames = self.term(params, rank)
243 horizontal = self.horizontal and self.horizontal(params, rank)
244 vertical = self.vertical and self.vertical(params, rank)
245 return frames, horizontal, vertical
246
247 def __repr__(self):
248 return "%s(%r, horizontal=%r, vertical=%r)" % (
249 type(self).__name__, self.term, self.horizontal, self.vertical)
250
251 class BulletDef(object):
252 """Bullet definition."""
253
254 direction = None
255 speed = None
256
257 def __init__(self, actions=[], direction=None, speed=None):
258 self.direction = direction
259 self.speed = speed
260 self.actions = list(actions)
261
262 @classmethod
263 def FromElement(cls, doc, element):
264 """Construct using an ElementTree-style element."""
265 actions = []
266 speed = None
267 direction = None
268 for subelem in element.getchildren():
269 tag = realtag(subelem)
270 if tag == "direction":
271 direction = Direction.FromElement(doc, subelem)
272 elif tag == "speed":
273 speed = Speed.FromElement(doc, subelem)
274 elif tag == "action":
275 actions.append(ActionDef.FromElement(doc, subelem))
276 elif tag == "actionRef":
277 actions.append(ActionRef.FromElement(doc, subelem))
278 dfn = cls(actions, direction, speed)
279 doc.bullets[element.get("label")] = dfn
280 return dfn
281
282 def __call__(self, params, rank):
283 actions = [action(params, rank) for action in self.actions]
284 return (
285 self.direction and self.direction(params, rank),
286 self.speed and self.speed(params, rank),
287 actions)
288
289 def __repr__(self):
290 return "%s(direction=%r, speed=%r, actions=%r)" % (
291 type(self).__name__, self.direction, self.speed, self.actions)
292
293 class BulletRef(object):
294 """Create a bullet by name with parameters."""
295
296 def __init__(self, bullet, params=None):
297 self.bullet = bullet
298 self.params = params or ParamList()
299
300 @classmethod
301 def FromElement(cls, doc, element):
302 """Construct using an ElementTree-style element."""
303 bullet = cls(element.get("label"), ParamList.FromElement(doc, element))
304 doc._bullet_refs.append(bullet)
305 return bullet
306
307 def __call__(self, params, rank):
308 return self.bullet(self.params(params, rank), rank)
309
310 def __repr__(self):
311 return "%s(params=%r, bullet=%r)" % (
312 type(self).__name__, self.params, self.bullet)
313
314 class ActionDef(object):
315 """Action definition."""
316
317 # This is self-referential, so it's filled in later.
318 CONSTRUCTORS = dict()
319
320 def __init__(self, actions):
321 self.actions = list(actions)
322
323 @classmethod
324 def FromElement(cls, doc, element):
325 """Construct using an ElementTree-style element."""
326 actions = []
327 for subelem in element.getchildren():
328 tag = realtag(subelem)
329 try:
330 ctr = cls.CONSTRUCTORS[tag]
331 except KeyError:
332 continue
333 else:
334 actions.append(ctr.FromElement(doc, subelem))
335 dfn = cls(actions)
336 doc.actions[element.get("label")] = dfn
337 return dfn
338
339 def __call__(self, params, rank):
340 return self.actions, params
341
342 def __repr__(self):
343 return "%s(%r)" % (type(self).__name__, self.actions)
344
345 class ActionRef(object):
346 """Run an action by name with parameters."""
347
348 def __init__(self, action, params=None):
349 self.action = action
350 self.params = params or ParamList()
351
352 @classmethod
353 def FromElement(cls, doc, element):
354 """Construct using an ElementTree-style element."""
355 action = cls(element.get("label"), ParamList.FromElement(doc, element))
356 doc._action_refs.append(action)
357 return action
358
359 def __call__(self, params, rank):
360 return self.action(self.params(params, rank), rank)
361
362 def __repr__(self):
363 return "%s(params=%r, action=%r)" % (
364 type(self).__name__, self.params, self.action)
365
366 class FireDef(object):
367 """Fire definition (creates a bullet)."""
368
369 def __init__(self, bullet, direction=None, speed=None):
370 self.bullet = bullet
371 self.direction = direction
372 self.speed = speed
373
374 @classmethod
375 def FromElement(cls, doc, element):
376 """Construct using an ElementTree-style element."""
377 direction = None
378 speed = None
379
380 for subelem in element.getchildren():
381 tag = realtag(subelem)
382 if tag == "direction":
383 direction = Direction.FromElement(doc, subelem, "aim")
384 elif tag == "speed":
385 speed = Speed.FromElement(doc, subelem)
386 elif tag == "bullet":
387 bullet = BulletDef.FromElement(doc, subelem)
388 elif tag == "bulletRef":
389 bullet = BulletRef.FromElement(doc, subelem)
390
391 try:
392 fire = cls(bullet, direction, speed)
393 except UnboundLocalError as exc:
394 raise ParseError(str(exc))
395 else:
396 doc.fires[element.get("label")] = fire
397 return fire
398
399 def __call__(self, params, rank):
400 direction, speed, actions = self.bullet(params, rank)
401 if self.direction:
402 direction = self.direction(params, rank)
403 if self.speed:
404 speed = self.speed(params, rank)
405 return direction, speed, actions
406
407 def __repr__(self):
408 return "%s(direction=%r, speed=%r, bullet=%r)" % (
409 type(self).__name__, self.direction, self.speed, self.bullet)
410
411 class FireRef(object):
412 """Fire a bullet by name with parameters."""
413
414 def __init__(self, fire, params=None):
415 self.fire = fire
416 self.params = params or ParamList()
417
418 @classmethod
419 def FromElement(cls, doc, element):
420 """Construct using an ElementTree-style element."""
421 fired = cls(element.get("label"), ParamList.FromElement(doc, element))
422 doc._fire_refs.append(fired)
423 return fired
424
425 def __call__(self, params, rank):
426 """Generate a Bullet from the FireDef and params."""
427 return self.fire(self.params(params, rank), rank)
428
429 def __repr__(self):
430 return "%s(params=%r, fire=%r)" % (
431 type(self).__name__, self.params, self.fire)
432
433 class BulletML(object):
434 """BulletML document.
435
436 A BulletML document is a collection of bullets, actions, and
437 firings, as well as a base game type.
438 """
439
440 CONSTRUCTORS = dict(
441 bullet=BulletDef,
442 action=ActionDef,
443 fire=FireDef,
444 )
445
446 def __init__(self, source):
447 self.bullets = {}
448 self.actions = {}
449 self.fires = {}
450
451 self._bullet_refs = []
452 self._action_refs = []
453 self._fire_refs = []
454
455 if isinstance(source, (str, unicode)):
456 source = StringIO(source)
457
458 tree = ElementTree()
459 root = tree.parse(source)
460
461 self.type = root.get("type", "none")
462
463 for element in root.getchildren():
464 tag = realtag(element)
465 if tag in self.CONSTRUCTORS:
466 self.CONSTRUCTORS[tag].FromElement(self, element)
467
468 try:
469 for ref in self._bullet_refs:
470 ref.bullet = self.bullets[ref.bullet]
471 for ref in self._fire_refs:
472 ref.fire = self.fires[ref.fire]
473 for ref in self._action_refs:
474 ref.action = self.actions[ref.action]
475 except KeyError as exc:
476 raise ParseError("unknown reference %s" % exc)
477
478 del(self._bullet_refs)
479 del(self._action_refs)
480 del(self._fire_refs)
481
482 self.bullets.pop(None, None)
483 self.actions.pop(None, None)
484 self.fires.pop(None, None)
485
486 @property
487 def top(self):
488 """Get a list of all top-level actions."""
489 return [dfn for name, dfn in self.actions.iteritems()
490 if name and name.startswith("top")]
491
492 def __repr__(self):
493 return "%s(type=%r, bullets=%r, actions=%r, fires=%r)" % (
494 type(self).__name__, self.type, self.bullets, self.actions,
495 self.fires)
496
497 ActionDef.CONSTRUCTORS = dict(
498 repeat=Repeat,
499 fire=FireDef,
500 fireRef=FireRef,
501 changeSpeed=ChangeSpeed,
502 changeDirection=ChangeDirection,
503 accel=Accel,
504 wait=Wait,
505 vanish=Vanish,
506 action=ActionDef,
507 actionRef=ActionRef)