Fix muscle memory writing bad code.
[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
28 __all__ = ["ParseError", "BulletML"]
29
30 class ParseError(Error):
31 """Raised when an error occurs parsing the XML structure."""
32 pass
33
34 def realtag(element):
35 """Strip namespace poop off the front of a tag."""
36 try:
37 return element.tag.rsplit('}', 1)[1]
38 except ValueError:
39 return element.tag
40
41 class ParamList(object):
42 """List of parameter definitions."""
43
44 def __init__(self, params=()):
45 self.params = list(params)
46
47 @classmethod
48 def FromXML(cls, doc, element):
49 """Construct using an ElementTree-style element."""
50 return cls([NumberDef(subelem.text) for subelem in element
51 if realtag(subelem) == "param"])
52
53 def __call__(self, params, rank):
54 return [param(params, rank) for param in self.params]
55
56 def __repr__(self):
57 return "%s(%r)" % (type(self).__name__, self.params)
58
59 class Direction(object):
60 """Raw direction value."""
61
62 VALID_TYPES = ["relative", "absolute", "aim", "sequence"]
63
64 def __init__(self, type, value):
65 if type not in self.VALID_TYPES:
66 raise ValueError("invalid type %r" % type)
67 self.type = type
68 self.value = value
69
70 def __getstate__(self):
71 return [('type', self.type), ('value', self.value.expr)]
72
73 def __setstate__(self, state):
74 state = dict(state)
75 self.__init__(state["type"], NumberDef(state["value"]))
76
77 @classmethod
78 def FromXML(cls, doc, element, default="absolute"):
79 """Construct using an ElementTree-style element."""
80 return cls(element.get("type", default), NumberDef(element.text))
81
82 def __call__(self, params, rank):
83 return (math.radians(self.value(params, rank)), self.type)
84
85 def __repr__(self):
86 return "%s(%r, type=%r)" % (
87 type(self).__name__, self.value, self.type)
88
89 class ChangeDirection(object):
90 """Direction change over time."""
91
92 def __init__(self, term, direction):
93 self.term = term
94 self.direction = direction
95
96 def __getstate__(self):
97 return [('frames', self.term.expr),
98 ('type', self.direction.type),
99 ('value', self.direction.value.expr)]
100
101 def __setstate__(self, state):
102 state = dict(state)
103 self.__init__(INumberDef(state["frames"]),
104 Direction(state["type"], NumberDef(state["value"])))
105
106 @classmethod
107 def FromXML(cls, doc, element):
108 """Construct using an ElementTree-style element."""
109 for subelem in element.getchildren():
110 tag = realtag(subelem)
111 if tag == "direction":
112 direction = Direction.FromXML(doc, subelem)
113 elif tag == "term":
114 term = INumberDef(subelem.text)
115 try:
116 return cls(term, direction)
117 except UnboundLocalError as exc:
118 raise ParseError(str(exc))
119
120 def __call__(self, params, rank):
121 return self.term(params, rank), self.direction(params, rank)
122
123 def __repr__(self):
124 return "%s(term=%r, direction=%r)" % (
125 type(self).__name__, self.term, self.direction)
126
127 class Speed(object):
128 """Raw speed value."""
129
130 VALID_TYPES = ["relative", "absolute", "sequence"]
131
132 def __init__(self, type, value):
133 if type not in self.VALID_TYPES:
134 raise ValueError("invalid type %r" % type)
135 self.type = type
136 self.value = value
137
138 def __getstate__(self):
139 return [('type', self.type), ('value', self.value.expr)]
140
141 def __setstate__(self, state):
142 state = dict(state)
143 self.__init__(state["type"], NumberDef(state["value"]))
144
145 @classmethod
146 def FromXML(cls, doc, element):
147 """Construct using an ElementTree-style element."""
148 return cls(element.get("type", "absolute"), NumberDef(element.text))
149
150 def __call__(self, params, rank):
151 return (self.value(params, rank), self.type)
152
153 def __repr__(self):
154 return "%s(%r, type=%r)" % (type(self).__name__, self.value, self.type)
155
156 class ChangeSpeed(object):
157 """Speed change over time."""
158
159 def __init__(self, term, speed):
160 self.term = term
161 self.speed = speed
162
163 def __getstate__(self):
164 return [('frames', self.term.expr),
165 ('type', self.speed.type),
166 ('value', self.speed.value.expr)]
167
168 def __setstate__(self, state):
169 state = dict(state)
170 self.__init__(INumberDef(state["frames"]),
171 Speed(state["type"], NumberDef(state["value"])))
172
173 @classmethod
174 def FromXML(cls, doc, element):
175 """Construct using an ElementTree-style element."""
176 for subelem in element.getchildren():
177 tag = realtag(subelem)
178 if tag == "speed":
179 speed = Speed.FromXML(doc, subelem)
180 elif tag == "term":
181 term = INumberDef(subelem.text)
182 try:
183 return cls(term, speed)
184 except UnboundLocalError as exc:
185 raise ParseError(str(exc))
186
187 def __call__(self, params, rank):
188 return self.term(params, rank), self.speed(params, rank)
189
190 def __repr__(self):
191 return "%s(term=%r, speed=%r)" % (
192 type(self).__name__, self.term, self.speed)
193
194 class Wait(object):
195 """Wait for some frames."""
196
197 def __init__(self, frames):
198 self.frames = frames
199
200 def __getstate__(self):
201 return dict(frames=self.frames.expr)
202
203 def __setstate__(self, state):
204 self.__init__(INumberDef(state["frames"]))
205
206 @classmethod
207 def FromXML(cls, doc, element):
208 """Construct using an ElementTree-style element."""
209 return cls(INumberDef(element.text))
210
211 def __call__(self, params, rank):
212 return self.frames(params, rank)
213
214 def __repr__(self):
215 return "%s(%r)" % (type(self).__name__, self.frames)
216
217 class Tag(object):
218 """Set a bullet tag."""
219
220 def __init__(self, tag):
221 self.tag = tag
222
223 def __getstate__(self):
224 return dict(tag=self.tag)
225
226 def __setstate__(self, state):
227 self.__init__(state["tag"])
228
229 @classmethod
230 def FromXML(cls, doc, element):
231 """Construct using an ElementTree-style element."""
232 return cls(element.text)
233
234 class Untag(object):
235 """Unset a bullet tag."""
236
237 def __init__(self, tag):
238 self.tag = tag
239
240 def __getstate__(self):
241 return dict(tag=self.tag)
242
243 def __setstate__(self, state):
244 self.__init__(state["tag"])
245
246 @classmethod
247 def FromXML(cls, doc, element):
248 """Construct using an ElementTree-style element."""
249 return cls(element.text)
250
251 class Vanish(object):
252 """Make the owner disappear."""
253
254 def __init__(self):
255 pass
256
257 @classmethod
258 def FromXML(cls, doc, element):
259 """Construct using an ElementTree-style element."""
260 return cls()
261
262 def __repr__(self):
263 return "%s()" % (type(self).__name__)
264
265 class Repeat(object):
266 """Repeat an action definition."""
267
268 def __init__(self, times, action):
269 self.times = times
270 self.action = action
271
272 def __getstate__(self):
273 return [('times', self.times.expr), ('action', self.action)]
274
275 def __setstate__(self, state):
276 state = dict(state)
277 self.__init__(INumberDef(state["times"]), state["action"])
278
279 @classmethod
280 def FromXML(cls, doc, element):
281 """Construct using an ElementTree-style element."""
282 for subelem in element.getchildren():
283 tag = realtag(subelem)
284 if tag == "times":
285 times = INumberDef(subelem.text)
286 elif tag == "action":
287 action = ActionDef.FromXML(doc, subelem)
288 elif tag == "actionRef":
289 action = ActionRef.FromXML(doc, subelem)
290 try:
291 return cls(times, action)
292 except UnboundLocalError as exc:
293 raise ParseError(str(exc))
294
295 def __call__(self, params, rank):
296 return self.times(params, rank), self.action(params, rank)
297
298 def __repr__(self):
299 return "%s(%r, %r)" % (type(self).__name__, self.times, self.action)
300
301 class Accel(object):
302 """Accelerate over some time."""
303
304 horizontal = None
305 vertical = None
306
307 def __init__(self, term, horizontal=None, vertical=None):
308 self.term = term
309 self.horizontal = horizontal
310 self.vertical = vertical
311
312 def __getstate__(self):
313 state = [('frames', self.term.expr)]
314 if self.horizontal:
315 state.append(('horizontal', self.horizontal))
316 if self.vertical:
317 state.append(('vertical', self.vertical))
318 return state
319
320 def __setstate__(self, state):
321 state = dict(state)
322 self.__init__(INumberDef(state["frames"]), state.get("horizontal"),
323 state.get("vertical"))
324
325 @classmethod
326 def FromXML(cls, doc, element):
327 """Construct using an ElementTree-style element."""
328 horizontal = None
329 vertical = None
330
331 for subelem in element.getchildren():
332 tag = realtag(subelem)
333 if tag == "term":
334 term = INumberDef(subelem.text)
335 elif tag == "horizontal":
336 horizontal = Speed.FromXML(doc, subelem)
337 elif tag == "vertical":
338 vertical = Speed.FromXML(doc, subelem)
339
340 try:
341 return cls(term, horizontal, vertical)
342 except AttributeError:
343 raise ParseError
344
345 def __call__(self, params, rank):
346 frames = self.term(params, rank)
347 horizontal = self.horizontal and self.horizontal(params, rank)
348 vertical = self.vertical and self.vertical(params, rank)
349 return frames, horizontal, vertical
350
351 def __repr__(self):
352 return "%s(%r, horizontal=%r, vertical=%r)" % (
353 type(self).__name__, self.term, self.horizontal, self.vertical)
354
355 class BulletDef(object):
356 """Bullet definition."""
357
358 direction = None
359 speed = None
360
361 def __init__(self, actions=(), direction=None, speed=None, tags=()):
362 self.direction = direction
363 self.speed = speed
364 self.actions = list(actions)
365 self.tags = set(tags)
366
367 def __getstate__(self):
368 state = []
369 if self.direction:
370 state.append(("direction", self.direction))
371 if self.speed:
372 state.append(("speed", self.speed))
373 if self.actions:
374 state.append(("actions", self.actions))
375 if self.tags:
376 state.append(("tags", list(self.tags)))
377 return state
378
379 def __setstate__(self, state):
380 state = dict(state)
381 self.__init__(**state)
382
383 @classmethod
384 def FromXML(cls, doc, element):
385 """Construct using an ElementTree-style element."""
386 actions = []
387 speed = None
388 direction = None
389 tags = set()
390 for subelem in element.getchildren():
391 tag = realtag(subelem)
392 if tag == "direction":
393 direction = Direction.FromXML(doc, subelem)
394 elif tag == "speed":
395 speed = Speed.FromXML(doc, subelem)
396 elif tag == "action":
397 actions.append(ActionDef.FromXML(doc, subelem))
398 elif tag == "actionRef":
399 actions.append(ActionRef.FromXML(doc, subelem))
400 elif tag == "tag":
401 tags.add(subelem.text)
402 dfn = cls(actions, direction, speed, tags)
403 doc._bullets[element.get("label")] = dfn
404 return dfn
405
406 def __call__(self, params, rank):
407 actions = [action(params, rank) for action in self.actions]
408 return (
409 self.direction and self.direction(params, rank),
410 self.speed and self.speed(params, rank),
411 self.tags,
412 actions)
413
414 def __repr__(self):
415 return "%s(direction=%r, speed=%r, actions=%r)" % (
416 type(self).__name__, self.direction, self.speed, self.actions)
417
418 class BulletRef(object):
419 """Create a bullet by name with parameters."""
420
421 def __init__(self, bullet, params=None):
422 self.bullet = bullet
423 self.params = ParamList() if params is None else params
424
425 def __getstate__(self):
426 state = []
427 if self.params.params:
428 params = [param.expr for param in self.params.params]
429 state.append(("params", params))
430 state.append(('bullet', self.bullet))
431 return state
432
433 def __setstate__(self, state):
434 state = dict(state)
435 bullet = state["bullet"]
436 params = [NumberDef(param) for param in state.get("params", [])]
437 self.__init__(bullet, ParamList(params))
438
439 @classmethod
440 def FromXML(cls, doc, element):
441 """Construct using an ElementTree-style element."""
442 bullet = cls(element.get("label"), ParamList.FromXML(doc, element))
443 doc._bullet_refs.append(bullet)
444 return bullet
445
446 def __call__(self, params, rank):
447 return self.bullet(self.params(params, rank), rank)
448
449 def __repr__(self):
450 return "%s(params=%r, bullet=%r)" % (
451 type(self).__name__, self.params, self.bullet)
452
453 class ActionDef(object):
454 """Action definition.
455
456 To support parsing new actions, add tags to
457 ActionDef.CONSTRUCTORS. It maps tag names to classes with a
458 FromXML classmethod, which take the BulletML instance and
459 ElementTree element as arguments.
460 """
461
462 # This is self-referential, so it's filled in later.
463 CONSTRUCTORS = dict()
464
465 def __init__(self, actions):
466 self.actions = list(actions)
467
468 def __getstate__(self):
469 return dict(actions=self.actions)
470
471 def __setstate__(self, state):
472 state = dict(state)
473 self.__init__(state["actions"])
474
475 @classmethod
476 def FromXML(cls, doc, element):
477 """Construct using an ElementTree-style element."""
478 actions = []
479 for subelem in element.getchildren():
480 tag = realtag(subelem)
481 try:
482 ctr = cls.CONSTRUCTORS[tag]
483 except KeyError:
484 continue
485 else:
486 actions.append(ctr.FromXML(doc, subelem))
487 dfn = cls(actions)
488 doc._actions[element.get("label")] = dfn
489 return dfn
490
491 def __call__(self, params, rank):
492 return self.actions, params
493
494 def __repr__(self):
495 return "%s(%r)" % (type(self).__name__, self.actions)
496
497 class ActionRef(object):
498 """Run an action by name with parameters."""
499
500 def __init__(self, action, params=None):
501 self.action = action
502 self.params = params or ParamList()
503
504 def __getstate__(self):
505 state = []
506 if self.params.params:
507 params = [param.expr for param in self.params.params]
508 state.append(("params", params))
509 state.append(('action', self.action))
510 return state
511
512 def __setstate__(self, state):
513 state = dict(state)
514 action = state["action"]
515 params = [NumberDef(param) for param in state.get("params", [])]
516 self.__init__(action, ParamList(params))
517
518 @classmethod
519 def FromXML(cls, doc, element):
520 """Construct using an ElementTree-style element."""
521 action = cls(element.get("label"), ParamList.FromXML(doc, element))
522 doc._action_refs.append(action)
523 return action
524
525 def __call__(self, params, rank):
526 return self.action(self.params(params, rank), rank)
527
528 def __repr__(self):
529 return "%s(params=%r, action=%r)" % (
530 type(self).__name__, self.params, self.action)
531
532 class Offset(object):
533 """Provide an offset to a bullet's initial position."""
534
535 VALID_TYPES = ["relative", "absolute"]
536
537 def __init__(self, type, x, y):
538 if type not in self.VALID_TYPES:
539 raise ValueError("invalid type %r" % type)
540 self.type = type
541 self.x = x
542 self.y = y
543
544 def __getstate__(self):
545 state = [('type', self.type)]
546 if self.x:
547 state.append(('x', self.x.expr))
548 if self.y:
549 state.append(('y', self.y.expr))
550 return state
551
552 def __setstate__(self, state):
553 state = dict(state)
554 self.__init__(state["type"], state.get("x"), state.get("y"))
555
556 @classmethod
557 def FromXML(cls, doc, element):
558 """Construct using an ElementTree-style element."""
559 type = element.get("type", "relative")
560 x = None
561 y = None
562 for subelem in element:
563 tag = realtag(subelem)
564 if tag == "x":
565 x = NumberDef(subelem.text)
566 elif tag == "y":
567 y = NumberDef(subelem.text)
568 return cls(type, x, y)
569
570 def __call__(self, params, rank):
571 return (self.x(params, rank) if self.x else 0,
572 self.y(params, rank) if self.y else 0)
573
574 class FireDef(object):
575 """Fire definition (creates a bullet)."""
576
577 def __init__(
578 self, bullet, direction=None, speed=None, offset=None, tags=()):
579 self.bullet = bullet
580 self.direction = direction
581 self.speed = speed
582 self.offset = offset
583 self.tags = set(tags)
584
585 def __getstate__(self):
586 state = []
587 if self.direction:
588 state.append(("direction", self.direction))
589 if self.speed:
590 state.append(("speed", self.speed))
591 if self.offset:
592 state.append(("offset", self.offset))
593 if self.tags:
594 state.append(("tags", list(self.tags)))
595 try:
596 params = self.bullet.params
597 except AttributeError:
598 state.append(('bullet', self.bullet))
599 else:
600 if params.params:
601 state.append(('bullet', self.bullet))
602 else:
603 # Strip out empty BulletRefs.
604 state.append(('bullet', self.bullet.bullet))
605 return state
606
607 def __setstate__(self, state):
608 state = dict(state)
609 self.__init__(**state)
610
611 @classmethod
612 def FromXML(cls, doc, element):
613 """Construct using an ElementTree-style element."""
614 direction = None
615 speed = None
616 offset = None
617 tags = set()
618
619 for subelem in element.getchildren():
620 tag = realtag(subelem)
621 if tag == "direction":
622 direction = Direction.FromXML(doc, subelem, "aim")
623 elif tag == "speed":
624 speed = Speed.FromXML(doc, subelem)
625 elif tag == "bullet":
626 bullet = BulletDef.FromXML(doc, subelem)
627 elif tag == "bulletRef":
628 bullet = BulletRef.FromXML(doc, subelem)
629 elif tag == "offset":
630 offset = Offset.FromXML(doc, subelem)
631 elif tag == "tag":
632 tags.add(subelem.text)
633 try:
634 fire = cls(bullet, direction, speed, offset, tags)
635 except UnboundLocalError as exc:
636 raise ParseError(str(exc))
637 else:
638 doc._fires[element.get("label")] = fire
639 return fire
640
641 def __call__(self, params, rank):
642 direction, speed, tags, actions = self.bullet(params, rank)
643 if self.direction:
644 direction = self.direction(params, rank)
645 if self.speed:
646 speed = self.speed(params, rank)
647 tags = tags.union(self.tags)
648 return direction, speed, self.offset, tags, actions
649
650 def __repr__(self):
651 return "%s(direction=%r, speed=%r, bullet=%r)" % (
652 type(self).__name__, self.direction, self.speed, self.bullet)
653
654 class FireRef(object):
655 """Fire a bullet by name with parameters."""
656
657 def __init__(self, fire, params=None):
658 self.fire = fire
659 self.params = params or ParamList()
660
661 def __getstate__(self):
662 state = []
663 if self.params.params:
664 params = [param.expr for param in self.params.params]
665 state.append(("params", params))
666 state.append(('fire', self.fire))
667 return state
668
669 def __setstate__(self, state):
670 state = dict(state)
671 fire = state["fire"]
672 params = [NumberDef(param) for param in state.get("params", [])]
673 self.__init__(fire, ParamList(params))
674
675 @classmethod
676 def FromXML(cls, doc, element):
677 """Construct using an ElementTree-style element."""
678 fired = cls(element.get("label"), ParamList.FromXML(doc, element))
679 doc._fire_refs.append(fired)
680 return fired
681
682 def __call__(self, params, rank):
683 return self.fire(self.params(params, rank), rank)
684
685 def __repr__(self):
686 return "%s(params=%r, fire=%r)" % (
687 type(self).__name__, self.params, self.fire)
688
689 class BulletML(object):
690 """BulletML document.
691
692 A BulletML document is a collection of top-level actions and the
693 base game type.
694
695 You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
696 its parsing. It maps tag names to classes with a FromXML
697 classmethod, which take the BulletML instance and ElementTree
698 element as arguments.
699
700 """
701
702 CONSTRUCTORS = dict(
703 bullet=BulletDef,
704 action=ActionDef,
705 fire=FireDef,
706 )
707
708 def __init__(self, type="none", actions=None):
709 self.type = type
710 self.actions = [] if actions is None else actions
711
712 def __getstate__(self):
713 return [('type', self.type), ('actions', self.actions)]
714
715 def __setstate__(self, state):
716 state = dict(state)
717 self.__init__(state["type"], actions=state.get("actions"))
718
719 @classmethod
720 def FromXML(cls, source):
721 """Return a BulletML instance based on XML."""
722 if not hasattr(source, 'read'):
723 source = StringIO(source)
724
725 tree = ElementTree()
726 root = tree.parse(source)
727
728 doc = cls(type=root.get("type", "none"))
729
730 doc._bullets = {}
731 doc._actions = {}
732 doc._fires = {}
733 doc._bullet_refs = []
734 doc._action_refs = []
735 doc._fire_refs = []
736
737 for element in root.getchildren():
738 tag = realtag(element)
739 if tag in doc.CONSTRUCTORS:
740 doc.CONSTRUCTORS[tag].FromXML(doc, element)
741
742 try:
743 for ref in doc._bullet_refs:
744 ref.bullet = doc._bullets[ref.bullet]
745 for ref in doc._fire_refs:
746 ref.fire = doc._fires[ref.fire]
747 for ref in doc._action_refs:
748 ref.action = doc._actions[ref.action]
749 except KeyError as exc:
750 raise ParseError("unknown reference %s" % exc)
751
752 doc.actions = [act for name, act in doc._actions.items()
753 if name and name.startswith("top")]
754
755 del(doc._bullet_refs)
756 del(doc._action_refs)
757 del(doc._fire_refs)
758 del(doc._bullets)
759 del(doc._actions)
760 del(doc._fires)
761
762 return doc
763
764 @classmethod
765 def FromYAML(cls, source):
766 """Create a BulletML instance based on YAML."""
767
768 # Late import to avoid a circular dependency.
769 try:
770 import bulletml.bulletyaml
771 import yaml
772 except ImportError:
773 raise ParseError("PyYAML is not available")
774 else:
775 try:
776 return yaml.load(source)
777 except Exception, exc:
778 raise ParseError(str(exc))
779
780 @classmethod
781 def FromDocument(cls, source):
782 """Create a BulletML instance based on a seekable file or string.
783
784 This attempts to autodetect if the stream is XML or YAML.
785 """
786 if not hasattr(source, 'read'):
787 source = StringIO(source)
788 start = source.read(1)
789 source.seek(0)
790 if start == "<":
791 return cls.FromXML(source)
792 elif start == "!" or start == "#":
793 return cls.FromYAML(source)
794 else:
795 raise ParseError("unknown initial character %r" % start)
796
797 def __repr__(self):
798 return "%s(type=%r, actions=%r)" % (
799 type(self).__name__, self.type, self.actions)
800
801 ActionDef.CONSTRUCTORS = dict(
802 repeat=Repeat,
803 fire=FireDef,
804 fireRef=FireRef,
805 changeSpeed=ChangeSpeed,
806 changeDirection=ChangeDirection,
807 accel=Accel,
808 wait=Wait,
809 vanish=Vanish,
810 tag=Tag,
811 untag=Untag,
812 action=ActionDef,
813 actionRef=ActionRef)