Bots build bots – building dsl for poisoning

Those who have been following me for a while will probably have figured out by now that I love domain specific languages. I love them to the point that I often spend lots of time tinkering with them, instead of working on things that I was originally working. Recently I replaced some Python code with Hy code and in the process wrote couple nifty macros.

Originally I had a subsystem written in Python (a language I really love and cherish) that I needed to change a bit. I decided to convert it into Hy (another language I love and cherish) first. This is what I had in the beginning (omitting imports, some decorators and docstrings):

class Poison(Effect):
    def __init__(self, duration, frequency, tick, damage, target,
                 icon, title, description):

        self.damage = damage = target
        self.effect_name = 'poison'

    def do_trigger(self, dying_rules): = - self.damage,


    def get_add_event(self):
        return new_poison_added_event(,

    def get_removal_event(self):
        return new_poison_ended_event(,

Basically, just a class inheriting Effect and redefining some of the methods. Baseclass defines some other methods, but they aren’t important for this blog post.

First version was direct translation from Python to Hy (using syntax valid for 0.11 version, in future class definitions and let forms will look slightly different):

(defclass Poison [Effect]
  [[--init-- (fn [self duration frequency tick damage target icon title
               (super-init :duration duration
                           :frequency frequency
                           :tick tick
                           :icon icon
                           :title title
                           :description description)
               (set-attributes damage target)
               (setv self.effect-name "poison")
   [do-trigger (fn [self dying-rules]
                 (let [[target]
                       [damage self.damage]]
                   (setv target.hit-points (- target.hit-points damage))
                   (.raise-event target (new-poison-triggered-event :target target
                                                                    :damage damage))
                   (.check-dying dying-rules target)))]
   [get-add-event (fn [self]
                    (new-poison-added-event :target
                                            :effect self))]
   [get-removal-event (fn [self]
                        (new-poison-ended-event :target
                                                :effect self))]])

The code uses two macros that make working with classes slightly easier: super-init and set-attributes. super-init is used to call –init– of baseclass with given set of arguments (I’m using keywords in this case, but positional arguments is supported too). set-attributes automates setting parameters from –init– to self, in this case it expands to:

  (setv self.damage damage)
  (setv target))

Not a big thing, but saves some tedious typing.

This was the point that got me thinking. I have several of these effect classes and in the future there will be more. All of them share the same structure: –init– with parameters (some common, some unique), do-trigger used to trigger the effect, get-add-event and get-removal-event that create event telling rest of the system that this effect was added or removed from a character. These common things could be extracted and made into mini-language, designed for defining these effects. I wrote what I envisioned that language look like in case of Poison effect:

(effect Poison "poison"
        [target damage]
        :trigger (do
                  (setv target.hit-points (- target.hit-points damage))
                  (.raise-event target (new-poison-triggered-event :target target
                                                                   :damage damage))
                  (.check-dying dying-rules target))
        :add-event (new-poison-added-event :target target
                                           :effect self)
        :remove-event (new-poison-ended-event :target target
                                              :effect self))

That piece of code should expand into the class definition shown before and for that I would need effect macro (and probably something more under the hood).

After quite a bit of experimenting, I ended up with the following:

(defmacro effect [type name attributes &rest body]
  "create a class that defines a new effect"
  (defn select-branch [x]
    (let [[key (first x)]
          [value (second x)]]
       [(= key :trigger) `(method do-trigger [dying-rules] ~attributes ~value)]
       [(= key :add-event) `(method get-add-event [] ~attributes ~value)]
       [(= key :remove-event) `(method get-removal-event [] ~attributes ~value)]
       [true (macro-error key "key failure")])))
  (defn pair-list [data]
    (list (zip (slice data 0 nil 2)
               (slice data 1 nil 2))))
  (let [[pairs (pair-list body)]]
    `(defclass ~type [Effect]
       [(effect-initializer ~name ~attributes)
        ~@(list-comp (select-branch pair)
                     [pair pairs])])))

This would create my class definition for me. Two helper functions are used to group keyword and corresponding code together and add correct method definition (this way I can omit some of the methods or define them in different order). The macro will also create –init– method with parameters common to all effects and the additional ones that are needed for a specific effect. This is done with effect-initializer macro that is given effect name and list of extra parameters:

(defmacro effect-initializer [effect-name attributes]
  "create --init-- method for effect"
  `[--init-- (fn [self duration frequency tick icon title
                  description ~@attributes]
               (super-init :duration duration
                           :frequency frequency
                           :tick tick
                           :icon icon
                           :title title
                           :description description)
               (set-attributes ~@attributes)
               (setv self.effect-name ~effect-name)

Edit: I kept getting funny error if I tried using set-attributes macro here, so I originally just wrote the generator expression to do the same work. After a night of sleep, I realised that I had passed the whole attributes list as single element, instead of splicing the content of it. The difference between ~attributes and ~@attributes is big deal after all.

Last step was to add methods to the class:

(defmacro method [name params attributes body]
  "create method used in effect class"
  `[~name (fn [self ~@params]
            (let [~@(genexpr `[~x (. self ~x)] [x attributes])]

Two out of three needed methods have identical signature. This is why I’m passing in params to define parameter list, so I can use the same macro for all three of the methods. Macro creates a method definition, with given signature and creates let form that binds extra attributes specific to this effect to respective local variables. This is done so we can refer to them without prepending them with “self.” or defining the let form by ourselves.

There are still some parts that I’m not completely happy or that I know will need some work in the future. One is that effects have multiples-allowed attribute that tells if a character can have multiple same kind of effects active. Current language doesn’t support setting it, so that’s something I need to add.

Another minor detail is that creating an event is pretty long. I should drop “new” and “event” from it:

(new-poison-triggered-event :target target
                            :damage damage)
(poison-triggered :target target
                  :damage damage))

All in all, I’m happy how the dsl is starting to form. I probably will never gain back the time I used to build it, but it was still an invaluable lesson in macros for me. And in the future when defclass and let syntax will change, I only need to update the macros and effects should be good to go.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s