Let’s make an Elemental Battle System in Haskell – Part IV


You can call this “EBS v. 3.5”, because I’m not showing here so much big improvements. I’m gonna focus on two main points:

  1. Some little refactoring (thanks to your comments)
  2. Implementation of custom spell behavior

A little less refactoring

Our already discussed isWeakTo and isStrongAgainst functions were relying on pattern matching in this fashion:

isWeakTo :: TargetableUnit -> Maybe Element -> Maybe Bool
m `isWeakTo` elem = case elem of
                         Just e -> checkProperty m ((==e) . succ)
                         _ -> Nothing

Uhm.. we are continuously putting in and getting out things from a context.. yes, this is a work for ours lovely Monads!

isWeakTo :: TargetableUnit -> Maybe Element -> Maybe Bool
m `isWeakTo` elem = elem >>= (\e -> checkProperty m ((==e) . succ))

Cool! Only two lines! But what are we really doing? Well, we take a value within a context (a Maybe Element) and we give it for lunch to our >>= operator: it takes our elem, unbox it (revealing an Element e or Nothing if nothing’s there), and giving it to our checkProperty function for dinner. We don’t have to put the result into a context again, because checkProperty returns a Maybe Bool, i.e. a Bool inside the failure context (Maybe). Pretty cool, isn’t it? Same applies for isStrongAgainst

A custom spell behavior

So far we have created a Spell, to cast against some TargetableUnit: we have a simple function called cast that takes a Spell and a TargetableUnit, and does the dirty job:

cast :: Spell -> TargetableUnit -> TargetableUnit
cast s t =
    let coeff = getDmgMult t (spellElem s)
        in case spellEffect s of
            Damage hit mana -> t {hp = hp t - floor (fromIntegral hit * coeff),
                                  mp = mp t - floor (fromIntegral mana * coeff)}
            Inflict statList -> let sList = status t
                                    in t {status = nub (sList ++ statList)}

But our spell type is quite limited: it can only inflict some damage or some status. As you may remember, some cool spells, or items, could do some special tricks based, for example on the target life. Let’s take two examples:

  1. An Elixir – Fully restores HP/MP
  2. The Laser spell (Do you remember? you can learn it from enemies through the Enemy Skill materia in FFVII) – cut by half enemy’s life

How can we do to implement this custom behavior? Fortunately,  the solution is pretty easy: let’s add a brand new SpellEffect!

--Essentially the result of a spell cast
data SpellEffect = Damage HitPoints ManaPoints
                 | Inflict [Status]
                 | Custom (TargetableUnit -> TargetableUnit)

--Essentially a magic
data Spell = Spell{spellName :: String,
                   spellDesc :: String,
                   spellCost :: Integer,
                   spellElem :: Maybe Element,
                   spellEffect :: SpellEffect}

instance Show Spell where
    show x = spellDesc x

Note: I’ve added the spellDesc because it can be pretty-printed with show. What have we done? We are currently saying that a Spell can do three things:

  1. Deals damage
  2. Inflicts some status
  3. A custom behavior, i.e. a function we could define and that be used to perform actions based of target properties!!
Let’s see how this reflects to our cast and use functions:
cast :: Spell -> TargetableUnit -> TargetableUnit
cast s t =
    let coeff = getDmgMult t (spellElem s)
        in case spellEffect s of
            Damage hit mana -> t {hp = hp t - floor (fromIntegral hit * coeff),
                                  mp = mp t - floor (fromIntegral mana * coeff)}
            Inflict statList -> let sList = status t
                                    in t {status = nub (sList ++ statList)}
            Custom f -> f $ t

--(INSIDE Item.hs)
data ItemEffect = Restore HitPoints ManaPoints
                | Cure [Status]
                | Custom (TargetableUnit -> TargetableUnit)

--Use the item i on the target t
use :: Item -> TargetableUnit -> TargetableUnit
use i t = case (itemEffect i) of
               (Restore hp' mp') -> t {hp = hp t + hp', mp = mp t + mp'}
               (Cure rList) -> t {status = filter (\s -> s `notElem` rList) $ status t}
               (Custom f) -> f $ t

Obviously same applies to Item type. Ok, cool! So let’s define 1 item and 1 spell, what about an Elixir and the laser spell?

--Inside Item.hs
elixirBehaviour :: TargetableUnit -> TargetableUnit
elixirBehaviour t = t {hp = maxHp t, mp = maxMp t}
elixir = Item "Elixir" "Fully restores HP and MP" (Custom elixirBehaviour)

--Inside Spell.hs
laserBehaviour :: TargetableUnit -> TargetableUnit
laserBehaviour t = t {hp = floor $ (fromIntegral $ maxHp t) / 2.0}
laser = Spell "Laser" "Cut by half target's health" 20 Nothing (Custom laserBehaviour)

Ok, now it’s time for the meat: let’s test them!

Ok, modules loaded: EBS.Main, EBS.Elemental, EBS.Target, EBS.Status, EBS.Spell, EBS.Item, EBS.MonsterPark.
ghci> piros
Unit {name = "Piros", level = 1, hp = 300, maxHp = 300, mp = 50, maxMp = 50, strength = 10, dexterity = 10, vitality = 10, magic = 10, spirit = 10, luck = 10, elemType = Just Fire, status = []}
ghci> let damaged_piros = cast laser piros
ghci> damaged_piros
Unit {name = "Piros", level = 1, hp = 150, maxHp = 300, mp = 50, maxMp = 50, strength = 10, dexterity = 10, vitality = 10, magic = 10, spirit = 10, luck = 10, elemType = Just Fire, status = []}
ghci> use elixir piros
Unit {name = "Piros", level = 1, hp = 300, maxHp = 300, mp = 50, maxMp = 50, strength = 10, dexterity = 10, vitality = 10, magic = 10, spirit = 10, luck = 10, elemType = Just Fire, status = []}

Wow, they works! The lamba wizard seems to approve:

Ah, bear in mind that I’ve changed the minimal context value of the Spell status to be [] (empty list), because [] is the minimal context for the lists, and we can use cool Monads tricks as usual!

Git repo is in the same place! Enjoy!

Alfredo

Rispondi

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo di WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione /  Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione /  Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione /  Modifica )

Connessione a %s...