Compound actions

Recently I decided to add a lunge action in Herculeum, which would make the character first move and then attack. This could be used to cover short distances and launching surprise attacks. Naturally I wanted to avoid duplication of code and set to look into how to combine two actions and execute them as one. Like quite often in software world a serendipitous event happened and I learned that there exists monad library for Hy called hymn. With some work on the code, I could use either monad to chain actions together in a rather neat way.

Following code example explains the idea (as of writing this, it’s still just an idea without a concrete implementation.) We have two actions available (move and attack) represented by their respective functions. There also exists a way to check if action is legal to perform in current situation. Functions return (Right character) when everything went smoothly and (Left character) when the action could not be performed for a reason or another. Using them like this isn’t any different from the current system.

(defn move [character direction]
  (if (move-legal? character direction)
    (do ...move character...
	    (Right character))
	(do ...make time pass a bit...
	    (Left character))))

(defn move-legal? [character direction]
  ...return true/false depending on if move is legal...)

(defn attack [character direction]
  (if (attack-legal? character direction)
    (do ...attack...
	    (Right character))
	(do ...make time pass a bit...
	    (Left character)))))

(defn attack-legal? [character direction]
  ...return true/false depending on if attack is legal...)

Where things get more interesting is when we want to create an action where character takes a step forward and performs an attack. We can neatly do this with hymn and do-monad macro as show below:

(defn lunge [character direction]
  (do-monad [moved (move character direction)
             attacked (attack moved direction)]

First character is moved to a given direction. If move is succesfull, an attack is performed to that same direction. In the end (Right character) is returned to signal success. However, if any of the steps fails for a reason or another (signaled by return value of (Left character)), process stops immediately and (Left character) is being returned from lunge function. Could be that our hero stepped on a trap while jumping forward and stumbled down and therefore can’t attack anymore.

Since each step modifies state of the world and can cause all kinds of nasty side-effects (like traps triggering and creatures dying), we can only check if the first step of the compound action is legal to perfom. Legality of rest of the steps is checked right before excecuting it, so they have to be written to take this into account:

(defn lunge-legal? [character direction]
  ...return true/false depending on if first step of lunge is legal...)

What is neat about compound actions, is that they can be combined to form new compound actions. For example, to have a longer leap before attack, one could write:

(defn leaping-attack [character direction]
  (do-monad [lunged (lunge character direction))
             attacked (attack lunged direction)]


(defn lunge [character direction]
  (do-monad [moved-1 (move character direction)
             moved-2 (move moved-1 direction)
             attacked (attack moved-2 direction)]

Nothing prevents us from doing other fanciful things in addition to just combining actions:

(defn sprint [character direction]
  (let [[tick character.tick]]
    (do-monad [moved-1 (move character direction)
               moved-2 (move moved-1 direction)
               final-character (set-tick moved-2 (+ tick 4)))]

Here we first store the original tick value (internal clock of character used in determining whose turn it’s to act next), move twice to given direction and set tick to slightly larger than the original. This will create a running character, who moves fast, can act often, but is really terrible at tight turns. The pretty much only requirement on these is that actions will return either Left or Right so that sequencing can work correctly.

EDIT: Philip Xu (author of Hymn) was very kind and contacted me with some suggestions on code examples.

In cases where the last function returns monad, one can use do-monad-m macro instead of do-monad:

(defn lunge [character direction]
  (do-monad-m [moved-1 (move character direction)
               moved-2 (move moved-1 direction)]
              (attack moved-2 direction)))

instead of

(defn lunge [character direction]
  (do-monad [moved-1 (move character direction)
             moved-2 (move moved-1 direction)
             attacked (attack moved-2 direction)]

Provided that character is already a monad, it is also possible to use partial application to have functions that accept single parameter and then use bind. In case character is just a plain object, one can turn it into monad simply by calling (Right character).

(defn lunge [character direction]
  (let [[move-d (partial move :direction direction)]
        [attack-d (partial attack :direction direction)]]
    (>> character move-d move-d attack-d)))

or use higher order functions like in:

(defn lunge [character direction]
  (let [[move-d (fn [c] (move c direction))]
        [attack-d (fn [c] (attack c direction))]]
    (>> character

2 thoughts on “Compound actions

  1. Pingback: Hymn 0.5 released | Engineer's Journey

  2. Pingback: Complex compound actions | Engineer's Journey

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