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