Tuesday, February 10, 2015

Replacement effects with partial functions

This is a very crude mockup of how replacement effects could look in Laterna Magica. Instead of any impressive effect, I'm just modifying a variable that represents a player's life total, but it does quite a lot for only being about 30 lines of code:

//a replaceable event
trait Event { def execute(): Unit }
//an effect that replaces an event
type ReplacementEffect = PartialFunction[Event, Event]

//applies all effects to the event. If a replacement doesn't match, the
//event remains unchanged
def replace(event: Event, effects: ReplacementEffect*) =
  effects.foldLeft(event) { case (event, effect) => effect.applyOrElse(event, identity[Event] _) }

var activeEffects: Seq[ReplacementEffect] = Nil
def execute(event: Event) = replace(event, activeEffects: _*).execute()

//example: A player's life points
var _life = 20
def life = _life

def gainLife(life: Int) = execute(GainLife(life))

case class GainLife(life: Int) extends Event {
  def execute() = _life += life


//gain three life
println(life) //23

//gain twice as much life
activeEffects = List({ case GainLife(x) => GainLife(x * 2) })

//gain six life
println(life) //29

The trait Event is used to represent replaceable events, and effects are simply "partial functions": functions that can only be applied to some of the values that its parameter type would allow. For example, at the bottom there's the partial function:

{ case GainLife(x) => GainLife(x * 2) }

While ReplacementEffect is defined for all Events, this one only accepts GainLife instances. Using PartialFunction.applyOrElse, I handle the cases where a replacement effect does not apply to an event.

The example is self-contained, so you should be able to run it yourself. In any case, the output is next to the print statements: The first time, you only get 3 life, but after registering the effect, the amount is indeed doubled. In reality, execute (and replace?) needs to be a bit smarter to recognize the right effects to apply, but otherwise this could stay almost as it is.


Hellfish said...

If I wasn't wholly unfamiliar with the syntax I would likely appreciate this more :) That being the case, how does this handle circular replacement, ie two Boon Reflections?

Hellfish said...

Nevermind my question, I'm dim and it's early in the morning and various other excuses lol. :P

Silly Freak said...

I know, functional syntax can be very different, and it doesn't get better by how much is going on in little code. ;)

For example, take this function:

Traversable[A].foldLeft[B](z: B)(op: (B, A) => B): B

Folds are a staple of functional programming. it takes a start value and a binary function - op: (B, A) => B - and repeatedly applies op to the intermediate result and one of the elements.
In my case, each element is itself a (partial) function, and I apply that function to the intermediate. If the function does not accept the parameter, I fall back to the identity function, which doesn't change its parameter.

Circular replacement looks more like an issue for another blog post - that I just wrote ;)