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