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