3 This is based on the format described at
4 http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/bulletml_ref_e.html.
6 Unless you are adding support for new tags, the only class you should
7 care about in here is BulletML.
10 from __future__
import division
14 from xml
.etree
.ElementTree
import ElementTree
17 from cStringIO
import StringIO
19 from StringIO
import StringIO
21 from bulletml
.errors
import Error
22 from bulletml
.expr
import NumberDef
, INumberDef
24 __all_
= ["ParseError", "BulletML"]
26 class ParseError(Error
):
27 """Raised when an error occurs parsing the XML structure."""
31 """Strip namespace poop off the front of a tag."""
33 return element
.tag
.rsplit('}', 1)[1]
37 class ParamList(object):
38 """List of parameter definitions."""
40 def __init__(self
, params
=()):
41 self
.params
= list(params
)
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"])
49 def __call__(self
, params
, rank
):
50 return [param(params
, rank
) for param
in self
.params
]
53 return "%s(%r)" % (type(self
).__name
__, self
.params
)
55 class Direction(object):
56 """Raw direction value."""
58 VALID_TYPES
= ["relative", "absolute", "aim", "sequence"]
60 def __init__(self
, type, value
):
61 if type not in self
.VALID_TYPES
:
62 raise ValueError("invalid type %r" % type)
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
))
71 def __call__(self
, params
, rank
):
72 return (math
.radians(self
.value(params
, rank
)), self
.type)
75 return "%s(%r, type=%r)" % (
76 type(self
).__name
__, self
.value
, self
.type)
78 class ChangeDirection(object):
79 """Direction change over time."""
81 def __init__(self
, term
, direction
):
83 self
.direction
= direction
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
)
93 term
= INumberDef(subelem
.text
)
95 return cls(term
, direction
)
96 except UnboundLocalError as exc
:
97 raise ParseError(str(exc
))
99 def __call__(self
, params
, rank
):
100 return self
.term(params
, rank
), self
.direction(params
, rank
)
103 return "%s(term=%r, direction=%r)" % (
104 type(self
).__name
__, self
.term
, self
.direction
)
107 """Raw speed value."""
109 VALID_TYPES
= ["relative", "absolute", "sequence"]
111 def __init__(self
, type, value
):
112 if type not in self
.VALID_TYPES
:
113 raise ValueError("invalid type %r" % type)
118 def FromElement(cls
, doc
, element
):
119 """Construct using an ElementTree-style element."""
120 return cls(element
.get("type", "absolute"), NumberDef(element
.text
))
122 def __call__(self
, params
, rank
):
123 return (self
.value(params
, rank
), self
.type)
126 return "%s(%r, type=%r)" % (type(self
).__name
__, self
.value
, self
.type)
128 class ChangeSpeed(object):
129 """Speed change over time."""
131 def __init__(self
, term
, speed
):
136 def FromElement(cls
, doc
, element
):
137 """Construct using an ElementTree-style element."""
138 for subelem
in element
.getchildren():
139 tag
= realtag(subelem
)
141 speed
= Speed
.FromElement(doc
, subelem
)
143 term
= INumberDef(subelem
.text
)
145 return cls(term
, speed
)
146 except UnboundLocalError as exc
:
147 raise ParseError(str(exc
))
149 def __call__(self
, params
, rank
):
150 return self
.term(params
, rank
), self
.speed(params
, rank
)
153 return "%s(term=%r, speed=%r)" % (
154 type(self
).__name
__, self
.term
, self
.speed
)
157 """Wait for some frames."""
159 def __init__(self
, frames
):
163 def FromElement(cls
, doc
, element
):
164 """Construct using an ElementTree-style element."""
165 return cls(INumberDef(element
.text
))
167 def __call__(self
, params
, rank
):
168 return self
.frames(params
, rank
)
171 return "%s(%r)" % (type(self
).__name
__, self
.frames
)
173 class Vanish(object):
174 """Make the owner disappear."""
180 def FromElement(cls
, doc
, element
):
181 """Construct using an ElementTree-style element."""
185 return "%s()" % (type(self
).__name
__)
187 class Repeat(object):
188 """Repeat an action definition."""
190 def __init__(self
, times
, action
):
195 def FromElement(cls
, doc
, element
):
196 """Construct using an ElementTree-style element."""
197 for subelem
in element
.getchildren():
198 tag
= realtag(subelem
)
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
)
206 return cls(times
, action
)
207 except UnboundLocalError as exc
:
208 raise ParseError(str(exc
))
210 def __call__(self
, params
, rank
):
211 return self
.times(params
, rank
), self
.action(params
, rank
)
214 return "%s(%r, %r)" % (type(self
).__name
__, self
.times
, self
.action
)
217 """Accelerate over some time."""
222 def __init__(self
, term
, horizontal
=None, vertical
=None):
224 self
.horizontal
= horizontal
225 self
.vertical
= vertical
228 def FromElement(cls
, doc
, element
):
229 """Construct using an ElementTree-style element."""
233 for subelem
in element
.getchildren():
234 tag
= realtag(subelem
)
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
)
243 return cls(term
, horizontal
, vertical
)
244 except AttributeError:
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
254 return "%s(%r, horizontal=%r, vertical=%r)" % (
255 type(self
).__name
__, self
.term
, self
.horizontal
, self
.vertical
)
257 class BulletDef(object):
258 """Bullet definition."""
263 def __init__(self
, actions
=[], direction
=None, speed
=None):
264 self
.direction
= direction
266 self
.actions
= list(actions
)
269 def FromElement(cls
, doc
, element
):
270 """Construct using an ElementTree-style element."""
274 for subelem
in element
.getchildren():
275 tag
= realtag(subelem
)
276 if tag
== "direction":
277 direction
= Direction
.FromElement(doc
, subelem
)
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
288 def __call__(self
, params
, rank
):
289 actions
= [action(params
, rank
) for action
in self
.actions
]
291 self
.direction
and self
.direction(params
, rank
),
292 self
.speed
and self
.speed(params
, rank
),
296 return "%s(direction=%r, speed=%r, actions=%r)" % (
297 type(self
).__name
__, self
.direction
, self
.speed
, self
.actions
)
299 class BulletRef(object):
300 """Create a bullet by name with parameters."""
302 def __init__(self
, bullet
, params
=None):
304 self
.params
= params
or ParamList()
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
)
313 def __call__(self
, params
, rank
):
314 return self
.bullet(self
.params(params
, rank
), rank
)
317 return "%s(params=%r, bullet=%r)" % (
318 type(self
).__name
__, self
.params
, self
.bullet
)
320 class ActionDef(object):
321 """Action definition.
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.
329 # This is self-referential, so it's filled in later.
330 CONSTRUCTORS
= dict()
332 def __init__(self
, actions
):
333 self
.actions
= list(actions
)
336 def FromElement(cls
, doc
, element
):
337 """Construct using an ElementTree-style element."""
339 for subelem
in element
.getchildren():
340 tag
= realtag(subelem
)
342 ctr
= cls
.CONSTRUCTORS
[tag
]
346 actions
.append(ctr
.FromElement(doc
, subelem
))
348 doc
.actions
[element
.get("label")] = dfn
351 def __call__(self
, params
, rank
):
352 return self
.actions
, params
355 return "%s(%r)" % (type(self
).__name
__, self
.actions
)
357 class ActionRef(object):
358 """Run an action by name with parameters."""
360 def __init__(self
, action
, params
=None):
362 self
.params
= params
or ParamList()
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
)
371 def __call__(self
, params
, rank
):
372 return self
.action(self
.params(params
, rank
), rank
)
375 return "%s(params=%r, action=%r)" % (
376 type(self
).__name
__, self
.params
, self
.action
)
378 class Offset(object):
379 """Provide an offset to a bullet's initial position."""
381 VALID_TYPES
= ["relative", "absolute"]
383 def __init__(self
, type, x
, y
):
384 if type not in self
.VALID_TYPES
:
385 raise ValueError("invalid type %r" % type)
391 def FromElement(cls
, doc
, element
):
392 """Construct using an ElementTree-style element."""
393 type = element
.get("type", "relative")
396 for subelem
in element
:
397 tag
= realtag(subelem
)
399 x
= NumberDef(subelem
.text
)
401 y
= NumberDef(subelem
.text
)
402 return cls(type, x
, y
)
404 def __call__(self
, params
, rank
):
405 return (self
.x(params
, rank
) if self
.x
else 0,
406 self
.y(params
, rank
) if self
.y
else 0)
408 class FireDef(object):
409 """Fire definition (creates a bullet)."""
411 def __init__(self
, bullet
, direction
=None, speed
=None, offset
=None):
413 self
.direction
= direction
418 def FromElement(cls
, doc
, element
):
419 """Construct using an ElementTree-style element."""
424 for subelem
in element
.getchildren():
425 tag
= realtag(subelem
)
426 if tag
== "direction":
427 direction
= Direction
.FromElement(doc
, subelem
, "aim")
429 speed
= Speed
.FromElement(doc
, subelem
)
430 elif tag
== "bullet":
431 bullet
= BulletDef
.FromElement(doc
, subelem
)
432 elif tag
== "bulletRef":
433 bullet
= BulletRef
.FromElement(doc
, subelem
)
434 elif tag
== "offset":
435 offset
= Offset
.FromElement(doc
, subelem
)
437 fire
= cls(bullet
, direction
, speed
, offset
)
438 except UnboundLocalError as exc
:
439 raise ParseError(str(exc
))
441 doc
.fires
[element
.get("label")] = fire
444 def __call__(self
, params
, rank
):
445 direction
, speed
, actions
= self
.bullet(params
, rank
)
447 direction
= self
.direction(params
, rank
)
449 speed
= self
.speed(params
, rank
)
450 return direction
, speed
, actions
, self
.offset
453 return "%s(direction=%r, speed=%r, bullet=%r)" % (
454 type(self
).__name
__, self
.direction
, self
.speed
, self
.bullet
)
456 class FireRef(object):
457 """Fire a bullet by name with parameters."""
459 def __init__(self
, fire
, params
=None):
461 self
.params
= params
or ParamList()
464 def FromElement(cls
, doc
, element
):
465 """Construct using an ElementTree-style element."""
466 fired
= cls(element
.get("label"), ParamList
.FromElement(doc
, element
))
467 doc
._fire
_refs
.append(fired
)
470 def __call__(self
, params
, rank
):
471 return self
.fire(self
.params(params
, rank
), rank
)
474 return "%s(params=%r, fire=%r)" % (
475 type(self
).__name
__, self
.params
, self
.fire
)
477 class BulletML(object):
478 """BulletML document.
480 A BulletML document is a collection of bullets, actions, and
481 firings, as well as a base game type.
483 You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
484 its parsing. It maps tag names to classes with a FromElement
485 classmethod, which take the BulletML instance and ElementTree
486 element as arguments.
496 def __init__(self
, type="none", bullets
=None, fires
=None, actions
=None):
498 self
.bullets
= {} if bullets
is None else bullets
499 self
.actions
= {} if actions
is None else actions
500 self
.fires
= {} if fires
is None else fires
503 def FromDocument(cls
, source
):
504 """Return a BulletML instance based on a string or file-like."""
505 if isinstance(source
, (str, unicode)):
506 source
= StringIO(source
)
509 root
= tree
.parse(source
)
511 self
= cls(type=root
.get("type", "none"))
513 self
._bullet
_refs
= []
514 self
._action
_refs
= []
517 for element
in root
.getchildren():
518 tag
= realtag(element
)
519 if tag
in self
.CONSTRUCTORS
:
520 self
.CONSTRUCTORS
[tag
].FromElement(self
, element
)
523 for ref
in self
._bullet
_refs
:
524 ref
.bullet
= self
.bullets
[ref
.bullet
]
525 for ref
in self
._fire
_refs
:
526 ref
.fire
= self
.fires
[ref
.fire
]
527 for ref
in self
._action
_refs
:
528 ref
.action
= self
.actions
[ref
.action
]
529 except KeyError as exc
:
530 raise ParseError("unknown reference %s" % exc
)
532 del(self
._bullet
_refs
)
533 del(self
._action
_refs
)
536 self
.bullets
.pop(None, None)
537 self
.actions
.pop(None, None)
538 self
.fires
.pop(None, None)
544 """Get a list of all top-level actions."""
545 return [dfn
for name
, dfn
in self
.actions
.iteritems()
546 if name
and name
.startswith("top")]
549 return "%s(type=%r, bullets=%r, actions=%r, fires=%r)" % (
550 type(self
).__name
__, self
.type, self
.bullets
, self
.actions
,
553 ActionDef
.CONSTRUCTORS
= dict(
557 changeSpeed
=ChangeSpeed
,
558 changeDirection
=ChangeDirection
,