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