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