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
)
174 """Set a bullet tag."""
176 def __init__(self
, tag
):
180 def FromElement(cls
, doc
, element
):
181 """Construct using an ElementTree-style element."""
182 return cls(element
.text
)
185 """Unset a bullet tag."""
187 def __init__(self
, tag
):
191 def FromElement(cls
, doc
, element
):
192 """Construct using an ElementTree-style element."""
193 return cls(element
.text
)
195 class Vanish(object):
196 """Make the owner disappear."""
202 def FromElement(cls
, doc
, element
):
203 """Construct using an ElementTree-style element."""
207 return "%s()" % (type(self
).__name
__)
209 class Repeat(object):
210 """Repeat an action definition."""
212 def __init__(self
, times
, action
):
217 def FromElement(cls
, doc
, element
):
218 """Construct using an ElementTree-style element."""
219 for subelem
in element
.getchildren():
220 tag
= realtag(subelem
)
222 times
= INumberDef(subelem
.text
)
223 elif tag
== "action":
224 action
= ActionDef
.FromElement(doc
, subelem
)
225 elif tag
== "actionRef":
226 action
= ActionRef
.FromElement(doc
, subelem
)
228 return cls(times
, action
)
229 except UnboundLocalError as exc
:
230 raise ParseError(str(exc
))
232 def __call__(self
, params
, rank
):
233 return self
.times(params
, rank
), self
.action(params
, rank
)
236 return "%s(%r, %r)" % (type(self
).__name
__, self
.times
, self
.action
)
239 """Accelerate over some time."""
244 def __init__(self
, term
, horizontal
=None, vertical
=None):
246 self
.horizontal
= horizontal
247 self
.vertical
= vertical
250 def FromElement(cls
, doc
, element
):
251 """Construct using an ElementTree-style element."""
255 for subelem
in element
.getchildren():
256 tag
= realtag(subelem
)
258 term
= INumberDef(subelem
.text
)
259 elif tag
== "horizontal":
260 horizontal
= Speed
.FromElement(doc
, subelem
)
261 elif tag
== "vertical":
262 vertical
= Speed
.FromElement(doc
, subelem
)
265 return cls(term
, horizontal
, vertical
)
266 except AttributeError:
269 def __call__(self
, params
, rank
):
270 frames
= self
.term(params
, rank
)
271 horizontal
= self
.horizontal
and self
.horizontal(params
, rank
)
272 vertical
= self
.vertical
and self
.vertical(params
, rank
)
273 return frames
, horizontal
, vertical
276 return "%s(%r, horizontal=%r, vertical=%r)" % (
277 type(self
).__name
__, self
.term
, self
.horizontal
, self
.vertical
)
279 class BulletDef(object):
280 """Bullet definition."""
285 def __init__(self
, actions
=[], direction
=None, speed
=None):
286 self
.direction
= direction
288 self
.actions
= list(actions
)
291 def FromElement(cls
, doc
, element
):
292 """Construct using an ElementTree-style element."""
296 for subelem
in element
.getchildren():
297 tag
= realtag(subelem
)
298 if tag
== "direction":
299 direction
= Direction
.FromElement(doc
, subelem
)
301 speed
= Speed
.FromElement(doc
, subelem
)
302 elif tag
== "action":
303 actions
.append(ActionDef
.FromElement(doc
, subelem
))
304 elif tag
== "actionRef":
305 actions
.append(ActionRef
.FromElement(doc
, subelem
))
306 dfn
= cls(actions
, direction
, speed
)
307 doc
.bullets
[element
.get("label")] = dfn
310 def __call__(self
, params
, rank
):
311 actions
= [action(params
, rank
) for action
in self
.actions
]
313 self
.direction
and self
.direction(params
, rank
),
314 self
.speed
and self
.speed(params
, rank
),
318 return "%s(direction=%r, speed=%r, actions=%r)" % (
319 type(self
).__name
__, self
.direction
, self
.speed
, self
.actions
)
321 class BulletRef(object):
322 """Create a bullet by name with parameters."""
324 def __init__(self
, bullet
, params
=None):
326 self
.params
= params
or ParamList()
329 def FromElement(cls
, doc
, element
):
330 """Construct using an ElementTree-style element."""
331 bullet
= cls(element
.get("label"), ParamList
.FromElement(doc
, element
))
332 doc
._bullet
_refs
.append(bullet
)
335 def __call__(self
, params
, rank
):
336 return self
.bullet(self
.params(params
, rank
), rank
)
339 return "%s(params=%r, bullet=%r)" % (
340 type(self
).__name
__, self
.params
, self
.bullet
)
342 class ActionDef(object):
343 """Action definition.
345 To support parsing new actions, add tags to
346 ActionDef.CONSTRUCTORS. It maps tag names to classes with a
347 FromElement classmethod, which take the BulletML instance and
348 ElementTree element as arguments.
351 # This is self-referential, so it's filled in later.
352 CONSTRUCTORS
= dict()
354 def __init__(self
, actions
):
355 self
.actions
= list(actions
)
358 def FromElement(cls
, doc
, element
):
359 """Construct using an ElementTree-style element."""
361 for subelem
in element
.getchildren():
362 tag
= realtag(subelem
)
364 ctr
= cls
.CONSTRUCTORS
[tag
]
368 actions
.append(ctr
.FromElement(doc
, subelem
))
370 doc
.actions
[element
.get("label")] = dfn
373 def __call__(self
, params
, rank
):
374 return self
.actions
, params
377 return "%s(%r)" % (type(self
).__name
__, self
.actions
)
379 class ActionRef(object):
380 """Run an action by name with parameters."""
382 def __init__(self
, action
, params
=None):
384 self
.params
= params
or ParamList()
387 def FromElement(cls
, doc
, element
):
388 """Construct using an ElementTree-style element."""
389 action
= cls(element
.get("label"), ParamList
.FromElement(doc
, element
))
390 doc
._action
_refs
.append(action
)
393 def __call__(self
, params
, rank
):
394 return self
.action(self
.params(params
, rank
), rank
)
397 return "%s(params=%r, action=%r)" % (
398 type(self
).__name
__, self
.params
, self
.action
)
400 class Offset(object):
401 """Provide an offset to a bullet's initial position."""
403 VALID_TYPES
= ["relative", "absolute"]
405 def __init__(self
, type, x
, y
):
406 if type not in self
.VALID_TYPES
:
407 raise ValueError("invalid type %r" % type)
413 def FromElement(cls
, doc
, element
):
414 """Construct using an ElementTree-style element."""
415 type = element
.get("type", "relative")
418 for subelem
in element
:
419 tag
= realtag(subelem
)
421 x
= NumberDef(subelem
.text
)
423 y
= NumberDef(subelem
.text
)
424 return cls(type, x
, y
)
426 def __call__(self
, params
, rank
):
427 return (self
.x(params
, rank
) if self
.x
else 0,
428 self
.y(params
, rank
) if self
.y
else 0)
430 class FireDef(object):
431 """Fire definition (creates a bullet)."""
433 def __init__(self
, bullet
, direction
=None, speed
=None, offset
=None):
435 self
.direction
= direction
440 def FromElement(cls
, doc
, element
):
441 """Construct using an ElementTree-style element."""
446 for subelem
in element
.getchildren():
447 tag
= realtag(subelem
)
448 if tag
== "direction":
449 direction
= Direction
.FromElement(doc
, subelem
, "aim")
451 speed
= Speed
.FromElement(doc
, subelem
)
452 elif tag
== "bullet":
453 bullet
= BulletDef
.FromElement(doc
, subelem
)
454 elif tag
== "bulletRef":
455 bullet
= BulletRef
.FromElement(doc
, subelem
)
456 elif tag
== "offset":
457 offset
= Offset
.FromElement(doc
, subelem
)
459 fire
= cls(bullet
, direction
, speed
, offset
)
460 except UnboundLocalError as exc
:
461 raise ParseError(str(exc
))
463 doc
.fires
[element
.get("label")] = fire
466 def __call__(self
, params
, rank
):
467 direction
, speed
, actions
= self
.bullet(params
, rank
)
469 direction
= self
.direction(params
, rank
)
471 speed
= self
.speed(params
, rank
)
472 return direction
, speed
, actions
, self
.offset
475 return "%s(direction=%r, speed=%r, bullet=%r)" % (
476 type(self
).__name
__, self
.direction
, self
.speed
, self
.bullet
)
478 class FireRef(object):
479 """Fire a bullet by name with parameters."""
481 def __init__(self
, fire
, params
=None):
483 self
.params
= params
or ParamList()
486 def FromElement(cls
, doc
, element
):
487 """Construct using an ElementTree-style element."""
488 fired
= cls(element
.get("label"), ParamList
.FromElement(doc
, element
))
489 doc
._fire
_refs
.append(fired
)
492 def __call__(self
, params
, rank
):
493 return self
.fire(self
.params(params
, rank
), rank
)
496 return "%s(params=%r, fire=%r)" % (
497 type(self
).__name
__, self
.params
, self
.fire
)
499 class BulletML(object):
500 """BulletML document.
502 A BulletML document is a collection of bullets, actions, and
503 firings, as well as a base game type.
505 You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
506 its parsing. It maps tag names to classes with a FromElement
507 classmethod, which take the BulletML instance and ElementTree
508 element as arguments.
518 def __init__(self
, type="none", bullets
=None, fires
=None, actions
=None):
520 self
.bullets
= {} if bullets
is None else bullets
521 self
.actions
= {} if actions
is None else actions
522 self
.fires
= {} if fires
is None else fires
525 def FromDocument(cls
, source
):
526 """Return a BulletML instance based on a string or file-like."""
527 if isinstance(source
, (str, unicode)):
528 source
= StringIO(source
)
531 root
= tree
.parse(source
)
533 self
= cls(type=root
.get("type", "none"))
535 self
._bullet
_refs
= []
536 self
._action
_refs
= []
539 for element
in root
.getchildren():
540 tag
= realtag(element
)
541 if tag
in self
.CONSTRUCTORS
:
542 self
.CONSTRUCTORS
[tag
].FromElement(self
, element
)
545 for ref
in self
._bullet
_refs
:
546 ref
.bullet
= self
.bullets
[ref
.bullet
]
547 for ref
in self
._fire
_refs
:
548 ref
.fire
= self
.fires
[ref
.fire
]
549 for ref
in self
._action
_refs
:
550 ref
.action
= self
.actions
[ref
.action
]
551 except KeyError as exc
:
552 raise ParseError("unknown reference %s" % exc
)
554 del(self
._bullet
_refs
)
555 del(self
._action
_refs
)
558 self
.bullets
.pop(None, None)
559 self
.actions
.pop(None, None)
560 self
.fires
.pop(None, None)
566 """Get a list of all top-level actions."""
567 return [dfn
for name
, dfn
in self
.actions
.iteritems()
568 if name
and name
.startswith("top")]
571 return "%s(type=%r, bullets=%r, actions=%r, fires=%r)" % (
572 type(self
).__name
__, self
.type, self
.bullets
, self
.actions
,
575 ActionDef
.CONSTRUCTORS
= dict(
579 changeSpeed
=ChangeSpeed
,
580 changeDirection
=ChangeDirection
,