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 io
import StringIO
20 from cStringIO
import StringIO
22 from StringIO
import StringIO
24 from bulletml
.errors
import Error
25 from bulletml
.expr
import NumberDef
, INumberDef
27 __all_
= ["ParseError", "BulletML"]
29 class ParseError(Error
):
30 """Raised when an error occurs parsing the XML structure."""
34 """Strip namespace poop off the front of a tag."""
36 return element
.tag
.rsplit('}', 1)[1]
40 class ParamList(object):
41 """List of parameter definitions."""
43 def __init__(self
, params
=()):
44 self
.params
= list(params
)
47 def FromElement(cls
, doc
, element
):
48 """Construct using an ElementTree-style element."""
49 return cls([NumberDef(subelem
.text
) for subelem
in element
50 if realtag(subelem
) == "param"])
52 def __call__(self
, params
, rank
):
53 return [param(params
, rank
) for param
in self
.params
]
56 return "%s(%r)" % (type(self
).__name
__, self
.params
)
58 class Direction(object):
59 """Raw direction value."""
61 VALID_TYPES
= ["relative", "absolute", "aim", "sequence"]
63 def __init__(self
, type, value
):
64 if type not in self
.VALID_TYPES
:
65 raise ValueError("invalid type %r" % type)
70 def FromElement(cls
, doc
, element
, default
="absolute"):
71 """Construct using an ElementTree-style element."""
72 return cls(element
.get("type", default
), NumberDef(element
.text
))
74 def __call__(self
, params
, rank
):
75 return (math
.radians(self
.value(params
, rank
)), self
.type)
78 return "%s(%r, type=%r)" % (
79 type(self
).__name
__, self
.value
, self
.type)
81 class ChangeDirection(object):
82 """Direction change over time."""
84 def __init__(self
, term
, direction
):
86 self
.direction
= direction
89 def FromElement(cls
, doc
, element
):
90 """Construct using an ElementTree-style element."""
91 for subelem
in element
.getchildren():
92 tag
= realtag(subelem
)
93 if tag
== "direction":
94 direction
= Direction
.FromElement(doc
, subelem
)
96 term
= INumberDef(subelem
.text
)
98 return cls(term
, direction
)
99 except UnboundLocalError as exc
:
100 raise ParseError(str(exc
))
102 def __call__(self
, params
, rank
):
103 return self
.term(params
, rank
), self
.direction(params
, rank
)
106 return "%s(term=%r, direction=%r)" % (
107 type(self
).__name
__, self
.term
, self
.direction
)
110 """Raw speed value."""
112 VALID_TYPES
= ["relative", "absolute", "sequence"]
114 def __init__(self
, type, value
):
115 if type not in self
.VALID_TYPES
:
116 raise ValueError("invalid type %r" % type)
121 def FromElement(cls
, doc
, element
):
122 """Construct using an ElementTree-style element."""
123 return cls(element
.get("type", "absolute"), NumberDef(element
.text
))
125 def __call__(self
, params
, rank
):
126 return (self
.value(params
, rank
), self
.type)
129 return "%s(%r, type=%r)" % (type(self
).__name
__, self
.value
, self
.type)
131 class ChangeSpeed(object):
132 """Speed change over time."""
134 def __init__(self
, term
, speed
):
139 def FromElement(cls
, doc
, element
):
140 """Construct using an ElementTree-style element."""
141 for subelem
in element
.getchildren():
142 tag
= realtag(subelem
)
144 speed
= Speed
.FromElement(doc
, subelem
)
146 term
= INumberDef(subelem
.text
)
148 return cls(term
, speed
)
149 except UnboundLocalError as exc
:
150 raise ParseError(str(exc
))
152 def __call__(self
, params
, rank
):
153 return self
.term(params
, rank
), self
.speed(params
, rank
)
156 return "%s(term=%r, speed=%r)" % (
157 type(self
).__name
__, self
.term
, self
.speed
)
160 """Wait for some frames."""
162 def __init__(self
, frames
):
166 def FromElement(cls
, doc
, element
):
167 """Construct using an ElementTree-style element."""
168 return cls(INumberDef(element
.text
))
170 def __call__(self
, params
, rank
):
171 return self
.frames(params
, rank
)
174 return "%s(%r)" % (type(self
).__name
__, self
.frames
)
177 """Set a bullet tag."""
179 def __init__(self
, tag
):
183 def FromElement(cls
, doc
, element
):
184 """Construct using an ElementTree-style element."""
185 return cls(element
.text
)
188 """Unset a bullet tag."""
190 def __init__(self
, tag
):
194 def FromElement(cls
, doc
, element
):
195 """Construct using an ElementTree-style element."""
196 return cls(element
.text
)
198 class Vanish(object):
199 """Make the owner disappear."""
205 def FromElement(cls
, doc
, element
):
206 """Construct using an ElementTree-style element."""
210 return "%s()" % (type(self
).__name
__)
212 class Repeat(object):
213 """Repeat an action definition."""
215 def __init__(self
, times
, action
):
220 def FromElement(cls
, doc
, element
):
221 """Construct using an ElementTree-style element."""
222 for subelem
in element
.getchildren():
223 tag
= realtag(subelem
)
225 times
= INumberDef(subelem
.text
)
226 elif tag
== "action":
227 action
= ActionDef
.FromElement(doc
, subelem
)
228 elif tag
== "actionRef":
229 action
= ActionRef
.FromElement(doc
, subelem
)
231 return cls(times
, action
)
232 except UnboundLocalError as exc
:
233 raise ParseError(str(exc
))
235 def __call__(self
, params
, rank
):
236 return self
.times(params
, rank
), self
.action(params
, rank
)
239 return "%s(%r, %r)" % (type(self
).__name
__, self
.times
, self
.action
)
242 """Accelerate over some time."""
247 def __init__(self
, term
, horizontal
=None, vertical
=None):
249 self
.horizontal
= horizontal
250 self
.vertical
= vertical
253 def FromElement(cls
, doc
, element
):
254 """Construct using an ElementTree-style element."""
258 for subelem
in element
.getchildren():
259 tag
= realtag(subelem
)
261 term
= INumberDef(subelem
.text
)
262 elif tag
== "horizontal":
263 horizontal
= Speed
.FromElement(doc
, subelem
)
264 elif tag
== "vertical":
265 vertical
= Speed
.FromElement(doc
, subelem
)
268 return cls(term
, horizontal
, vertical
)
269 except AttributeError:
272 def __call__(self
, params
, rank
):
273 frames
= self
.term(params
, rank
)
274 horizontal
= self
.horizontal
and self
.horizontal(params
, rank
)
275 vertical
= self
.vertical
and self
.vertical(params
, rank
)
276 return frames
, horizontal
, vertical
279 return "%s(%r, horizontal=%r, vertical=%r)" % (
280 type(self
).__name
__, self
.term
, self
.horizontal
, self
.vertical
)
282 class BulletDef(object):
283 """Bullet definition."""
288 def __init__(self
, actions
=[], direction
=None, speed
=None):
289 self
.direction
= direction
291 self
.actions
= list(actions
)
294 def FromElement(cls
, doc
, element
):
295 """Construct using an ElementTree-style element."""
299 for subelem
in element
.getchildren():
300 tag
= realtag(subelem
)
301 if tag
== "direction":
302 direction
= Direction
.FromElement(doc
, subelem
)
304 speed
= Speed
.FromElement(doc
, subelem
)
305 elif tag
== "action":
306 actions
.append(ActionDef
.FromElement(doc
, subelem
))
307 elif tag
== "actionRef":
308 actions
.append(ActionRef
.FromElement(doc
, subelem
))
309 dfn
= cls(actions
, direction
, speed
)
310 doc
.bullets
[element
.get("label")] = dfn
313 def __call__(self
, params
, rank
):
314 actions
= [action(params
, rank
) for action
in self
.actions
]
316 self
.direction
and self
.direction(params
, rank
),
317 self
.speed
and self
.speed(params
, rank
),
321 return "%s(direction=%r, speed=%r, actions=%r)" % (
322 type(self
).__name
__, self
.direction
, self
.speed
, self
.actions
)
324 class BulletRef(object):
325 """Create a bullet by name with parameters."""
327 def __init__(self
, bullet
, params
=None):
329 self
.params
= params
or ParamList()
332 def FromElement(cls
, doc
, element
):
333 """Construct using an ElementTree-style element."""
334 bullet
= cls(element
.get("label"), ParamList
.FromElement(doc
, element
))
335 doc
._bullet
_refs
.append(bullet
)
338 def __call__(self
, params
, rank
):
339 return self
.bullet(self
.params(params
, rank
), rank
)
342 return "%s(params=%r, bullet=%r)" % (
343 type(self
).__name
__, self
.params
, self
.bullet
)
345 class ActionDef(object):
346 """Action definition.
348 To support parsing new actions, add tags to
349 ActionDef.CONSTRUCTORS. It maps tag names to classes with a
350 FromElement classmethod, which take the BulletML instance and
351 ElementTree element as arguments.
354 # This is self-referential, so it's filled in later.
355 CONSTRUCTORS
= dict()
357 def __init__(self
, actions
):
358 self
.actions
= list(actions
)
361 def FromElement(cls
, doc
, element
):
362 """Construct using an ElementTree-style element."""
364 for subelem
in element
.getchildren():
365 tag
= realtag(subelem
)
367 ctr
= cls
.CONSTRUCTORS
[tag
]
371 actions
.append(ctr
.FromElement(doc
, subelem
))
373 doc
.actions
[element
.get("label")] = dfn
376 def __call__(self
, params
, rank
):
377 return self
.actions
, params
380 return "%s(%r)" % (type(self
).__name
__, self
.actions
)
382 class ActionRef(object):
383 """Run an action by name with parameters."""
385 def __init__(self
, action
, params
=None):
387 self
.params
= params
or ParamList()
390 def FromElement(cls
, doc
, element
):
391 """Construct using an ElementTree-style element."""
392 action
= cls(element
.get("label"), ParamList
.FromElement(doc
, element
))
393 doc
._action
_refs
.append(action
)
396 def __call__(self
, params
, rank
):
397 return self
.action(self
.params(params
, rank
), rank
)
400 return "%s(params=%r, action=%r)" % (
401 type(self
).__name
__, self
.params
, self
.action
)
403 class Offset(object):
404 """Provide an offset to a bullet's initial position."""
406 VALID_TYPES
= ["relative", "absolute"]
408 def __init__(self
, type, x
, y
):
409 if type not in self
.VALID_TYPES
:
410 raise ValueError("invalid type %r" % type)
416 def FromElement(cls
, doc
, element
):
417 """Construct using an ElementTree-style element."""
418 type = element
.get("type", "relative")
421 for subelem
in element
:
422 tag
= realtag(subelem
)
424 x
= NumberDef(subelem
.text
)
426 y
= NumberDef(subelem
.text
)
427 return cls(type, x
, y
)
429 def __call__(self
, params
, rank
):
430 return (self
.x(params
, rank
) if self
.x
else 0,
431 self
.y(params
, rank
) if self
.y
else 0)
433 class FireDef(object):
434 """Fire definition (creates a bullet)."""
436 def __init__(self
, bullet
, direction
=None, speed
=None, offset
=None):
438 self
.direction
= direction
443 def FromElement(cls
, doc
, element
):
444 """Construct using an ElementTree-style element."""
449 for subelem
in element
.getchildren():
450 tag
= realtag(subelem
)
451 if tag
== "direction":
452 direction
= Direction
.FromElement(doc
, subelem
, "aim")
454 speed
= Speed
.FromElement(doc
, subelem
)
455 elif tag
== "bullet":
456 bullet
= BulletDef
.FromElement(doc
, subelem
)
457 elif tag
== "bulletRef":
458 bullet
= BulletRef
.FromElement(doc
, subelem
)
459 elif tag
== "offset":
460 offset
= Offset
.FromElement(doc
, subelem
)
462 fire
= cls(bullet
, direction
, speed
, offset
)
463 except UnboundLocalError as exc
:
464 raise ParseError(str(exc
))
466 doc
.fires
[element
.get("label")] = fire
469 def __call__(self
, params
, rank
):
470 direction
, speed
, actions
= self
.bullet(params
, rank
)
472 direction
= self
.direction(params
, rank
)
474 speed
= self
.speed(params
, rank
)
475 return direction
, speed
, actions
, self
.offset
478 return "%s(direction=%r, speed=%r, bullet=%r)" % (
479 type(self
).__name
__, self
.direction
, self
.speed
, self
.bullet
)
481 class FireRef(object):
482 """Fire a bullet by name with parameters."""
484 def __init__(self
, fire
, params
=None):
486 self
.params
= params
or ParamList()
489 def FromElement(cls
, doc
, element
):
490 """Construct using an ElementTree-style element."""
491 fired
= cls(element
.get("label"), ParamList
.FromElement(doc
, element
))
492 doc
._fire
_refs
.append(fired
)
495 def __call__(self
, params
, rank
):
496 return self
.fire(self
.params(params
, rank
), rank
)
499 return "%s(params=%r, fire=%r)" % (
500 type(self
).__name
__, self
.params
, self
.fire
)
502 class BulletML(object):
503 """BulletML document.
505 A BulletML document is a collection of bullets, actions, and
506 firings, as well as a base game type.
508 You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
509 its parsing. It maps tag names to classes with a FromElement
510 classmethod, which take the BulletML instance and ElementTree
511 element as arguments.
521 def __init__(self
, type="none", bullets
=None, fires
=None, actions
=None):
523 self
.bullets
= {} if bullets
is None else bullets
524 self
.actions
= {} if actions
is None else actions
525 self
.fires
= {} if fires
is None else fires
528 def FromDocument(cls
, source
):
529 """Return a BulletML instance based on a string or file-like."""
530 if not hasattr(source
, 'read'):
531 source
= StringIO(source
)
534 root
= tree
.parse(source
)
536 self
= cls(type=root
.get("type", "none"))
538 self
._bullet
_refs
= []
539 self
._action
_refs
= []
542 for element
in root
.getchildren():
543 tag
= realtag(element
)
544 if tag
in self
.CONSTRUCTORS
:
545 self
.CONSTRUCTORS
[tag
].FromElement(self
, element
)
548 for ref
in self
._bullet
_refs
:
549 ref
.bullet
= self
.bullets
[ref
.bullet
]
550 for ref
in self
._fire
_refs
:
551 ref
.fire
= self
.fires
[ref
.fire
]
552 for ref
in self
._action
_refs
:
553 ref
.action
= self
.actions
[ref
.action
]
554 except KeyError as exc
:
555 raise ParseError("unknown reference %s" % exc
)
557 del(self
._bullet
_refs
)
558 del(self
._action
_refs
)
561 self
.bullets
.pop(None, None)
562 self
.actions
.pop(None, None)
563 self
.fires
.pop(None, None)
569 """Get a list of all top-level actions."""
570 return [dfn
for name
, dfn
in self
.actions
.items()
571 if name
and name
.startswith("top")]
574 return "%s(type=%r, bullets=%r, actions=%r, fires=%r)" % (
575 type(self
).__name
__, self
.type, self
.bullets
, self
.actions
,
578 ActionDef
.CONSTRUCTORS
= dict(
582 changeSpeed
=ChangeSpeed
,
583 changeDirection
=ChangeDirection
,