Patter editing.
[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 Vanish(object):
174 """Make the owner disappear."""
175
176 def __init__(self):
177 pass
178
179 @classmethod
180 def FromElement(cls, doc, element):
181 """Construct using an ElementTree-style element."""
182 return cls()
183
184 def __repr__(self):
185 return "%s()" % (type(self).__name__)
186
187 class Repeat(object):
188 """Repeat an action definition."""
189
190 def __init__(self, times, action):
191 self.times = times
192 self.action = action
193
194 @classmethod
195 def FromElement(cls, doc, element):
196 """Construct using an ElementTree-style element."""
197 for subelem in element.getchildren():
198 tag = realtag(subelem)
199 if tag == "times":
200 times = INumberDef(subelem.text)
201 elif tag == "action":
202 action = ActionDef.FromElement(doc, subelem)
203 elif tag == "actionRef":
204 action = ActionRef.FromElement(doc, subelem)
205 try:
206 return cls(times, action)
207 except UnboundLocalError as exc:
208 raise ParseError(str(exc))
209
210 def __call__(self, params, rank):
211 return self.times(params, rank), self.action(params, rank)
212
213 def __repr__(self):
214 return "%s(%r, %r)" % (type(self).__name__, self.times, self.action)
215
216 class Accel(object):
217 """Accelerate over some time."""
218
219 horizontal = None
220 vertical = None
221
222 def __init__(self, term, horizontal=None, vertical=None):
223 self.term = term
224 self.horizontal = horizontal
225 self.vertical = vertical
226
227 @classmethod
228 def FromElement(cls, doc, element):
229 """Construct using an ElementTree-style element."""
230 horizontal = None
231 vertical = None
232
233 for subelem in element.getchildren():
234 tag = realtag(subelem)
235 if tag == "term":
236 term = INumberDef(subelem.text)
237 elif tag == "horizontal":
238 horizontal = Speed.FromElement(doc, subelem)
239 elif tag == "vertical":
240 vertical = Speed.FromElement(doc, subelem)
241
242 try:
243 return cls(term, horizontal, vertical)
244 except AttributeError:
245 raise ParseError
246
247 def __call__(self, params, rank):
248 frames = self.term(params, rank)
249 horizontal = self.horizontal and self.horizontal(params, rank)
250 vertical = self.vertical and self.vertical(params, rank)
251 return frames, horizontal, vertical
252
253 def __repr__(self):
254 return "%s(%r, horizontal=%r, vertical=%r)" % (
255 type(self).__name__, self.term, self.horizontal, self.vertical)
256
257 class BulletDef(object):
258 """Bullet definition."""
259
260 direction = None
261 speed = None
262
263 def __init__(self, actions=[], direction=None, speed=None):
264 self.direction = direction
265 self.speed = speed
266 self.actions = list(actions)
267
268 @classmethod
269 def FromElement(cls, doc, element):
270 """Construct using an ElementTree-style element."""
271 actions = []
272 speed = None
273 direction = None
274 for subelem in element.getchildren():
275 tag = realtag(subelem)
276 if tag == "direction":
277 direction = Direction.FromElement(doc, subelem)
278 elif tag == "speed":
279 speed = Speed.FromElement(doc, subelem)
280 elif tag == "action":
281 actions.append(ActionDef.FromElement(doc, subelem))
282 elif tag == "actionRef":
283 actions.append(ActionRef.FromElement(doc, subelem))
284 dfn = cls(actions, direction, speed)
285 doc.bullets[element.get("label")] = dfn
286 return dfn
287
288 def __call__(self, params, rank):
289 actions = [action(params, rank) for action in self.actions]
290 return (
291 self.direction and self.direction(params, rank),
292 self.speed and self.speed(params, rank),
293 actions)
294
295 def __repr__(self):
296 return "%s(direction=%r, speed=%r, actions=%r)" % (
297 type(self).__name__, self.direction, self.speed, self.actions)
298
299 class BulletRef(object):
300 """Create a bullet by name with parameters."""
301
302 def __init__(self, bullet, params=None):
303 self.bullet = bullet
304 self.params = params or ParamList()
305
306 @classmethod
307 def FromElement(cls, doc, element):
308 """Construct using an ElementTree-style element."""
309 bullet = cls(element.get("label"), ParamList.FromElement(doc, element))
310 doc._bullet_refs.append(bullet)
311 return bullet
312
313 def __call__(self, params, rank):
314 return self.bullet(self.params(params, rank), rank)
315
316 def __repr__(self):
317 return "%s(params=%r, bullet=%r)" % (
318 type(self).__name__, self.params, self.bullet)
319
320 class ActionDef(object):
321 """Action definition.
322
323 To support parsing new actions, add tags to
324 ActionDef.CONSTRUCTORS. It maps tag names to classes with a
325 FromElement classmethod, which take the BulletML instance and
326 ElementTree element as arguments.
327 """
328
329 # This is self-referential, so it's filled in later.
330 CONSTRUCTORS = dict()
331
332 def __init__(self, actions):
333 self.actions = list(actions)
334
335 @classmethod
336 def FromElement(cls, doc, element):
337 """Construct using an ElementTree-style element."""
338 actions = []
339 for subelem in element.getchildren():
340 tag = realtag(subelem)
341 try:
342 ctr = cls.CONSTRUCTORS[tag]
343 except KeyError:
344 continue
345 else:
346 actions.append(ctr.FromElement(doc, subelem))
347 dfn = cls(actions)
348 doc.actions[element.get("label")] = dfn
349 return dfn
350
351 def __call__(self, params, rank):
352 return self.actions, params
353
354 def __repr__(self):
355 return "%s(%r)" % (type(self).__name__, self.actions)
356
357 class ActionRef(object):
358 """Run an action by name with parameters."""
359
360 def __init__(self, action, params=None):
361 self.action = action
362 self.params = params or ParamList()
363
364 @classmethod
365 def FromElement(cls, doc, element):
366 """Construct using an ElementTree-style element."""
367 action = cls(element.get("label"), ParamList.FromElement(doc, element))
368 doc._action_refs.append(action)
369 return action
370
371 def __call__(self, params, rank):
372 return self.action(self.params(params, rank), rank)
373
374 def __repr__(self):
375 return "%s(params=%r, action=%r)" % (
376 type(self).__name__, self.params, self.action)
377
378 class FireDef(object):
379 """Fire definition (creates a bullet)."""
380
381 def __init__(self, bullet, direction=None, speed=None):
382 self.bullet = bullet
383 self.direction = direction
384 self.speed = speed
385
386 @classmethod
387 def FromElement(cls, doc, element):
388 """Construct using an ElementTree-style element."""
389 direction = None
390 speed = None
391
392 for subelem in element.getchildren():
393 tag = realtag(subelem)
394 if tag == "direction":
395 direction = Direction.FromElement(doc, subelem, "aim")
396 elif tag == "speed":
397 speed = Speed.FromElement(doc, subelem)
398 elif tag == "bullet":
399 bullet = BulletDef.FromElement(doc, subelem)
400 elif tag == "bulletRef":
401 bullet = BulletRef.FromElement(doc, subelem)
402
403 try:
404 fire = cls(bullet, direction, speed)
405 except UnboundLocalError as exc:
406 raise ParseError(str(exc))
407 else:
408 doc.fires[element.get("label")] = fire
409 return fire
410
411 def __call__(self, params, rank):
412 direction, speed, actions = self.bullet(params, rank)
413 if self.direction:
414 direction = self.direction(params, rank)
415 if self.speed:
416 speed = self.speed(params, rank)
417 return direction, speed, actions
418
419 def __repr__(self):
420 return "%s(direction=%r, speed=%r, bullet=%r)" % (
421 type(self).__name__, self.direction, self.speed, self.bullet)
422
423 class FireRef(object):
424 """Fire a bullet by name with parameters."""
425
426 def __init__(self, fire, params=None):
427 self.fire = fire
428 self.params = params or ParamList()
429
430 @classmethod
431 def FromElement(cls, doc, element):
432 """Construct using an ElementTree-style element."""
433 fired = cls(element.get("label"), ParamList.FromElement(doc, element))
434 doc._fire_refs.append(fired)
435 return fired
436
437 def __call__(self, params, rank):
438 """Generate a Bullet from the FireDef and params."""
439 return self.fire(self.params(params, rank), rank)
440
441 def __repr__(self):
442 return "%s(params=%r, fire=%r)" % (
443 type(self).__name__, self.params, self.fire)
444
445 class BulletML(object):
446 """BulletML document.
447
448 A BulletML document is a collection of bullets, actions, and
449 firings, as well as a base game type.
450
451 You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
452 its parsing. It maps tag names to classes with a FromElement
453 classmethod, which take the BulletML instance and ElementTree
454 element as arguments.
455
456 """
457
458 CONSTRUCTORS = dict(
459 bullet=BulletDef,
460 action=ActionDef,
461 fire=FireDef,
462 )
463
464 def __init__(self, type="none", bullets=None, fires=None, actions=None):
465 self.type = type
466 self.bullets = {} if bullets is None else bullets
467 self.actions = {} if actions is None else actions
468 self.fires = {} if fires is None else fires
469
470 @classmethod
471 def FromDocument(cls, source):
472 """Return a BulletML instance based on a string or file-like."""
473 if isinstance(source, (str, unicode)):
474 source = StringIO(source)
475
476 tree = ElementTree()
477 root = tree.parse(source)
478
479 self = cls(type=root.get("type", "none"))
480
481 self._bullet_refs = []
482 self._action_refs = []
483 self._fire_refs = []
484
485 for element in root.getchildren():
486 tag = realtag(element)
487 if tag in self.CONSTRUCTORS:
488 self.CONSTRUCTORS[tag].FromElement(self, element)
489
490 try:
491 for ref in self._bullet_refs:
492 ref.bullet = self.bullets[ref.bullet]
493 for ref in self._fire_refs:
494 ref.fire = self.fires[ref.fire]
495 for ref in self._action_refs:
496 ref.action = self.actions[ref.action]
497 except KeyError as exc:
498 raise ParseError("unknown reference %s" % exc)
499
500 del(self._bullet_refs)
501 del(self._action_refs)
502 del(self._fire_refs)
503
504 self.bullets.pop(None, None)
505 self.actions.pop(None, None)
506 self.fires.pop(None, None)
507
508 return self
509
510 @property
511 def top(self):
512 """Get a list of all top-level actions."""
513 return [dfn for name, dfn in self.actions.iteritems()
514 if name and name.startswith("top")]
515
516 def __repr__(self):
517 return "%s(type=%r, bullets=%r, actions=%r, fires=%r)" % (
518 type(self).__name__, self.type, self.bullets, self.actions,
519 self.fires)
520
521 ActionDef.CONSTRUCTORS = dict(
522 repeat=Repeat,
523 fire=FireDef,
524 fireRef=FireRef,
525 changeSpeed=ChangeSpeed,
526 changeDirection=ChangeDirection,
527 accel=Accel,
528 wait=Wait,
529 vanish=Vanish,
530 action=ActionDef,
531 actionRef=ActionRef)