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


Hi everyone,

in the last post we have played a bit with TargetableUnit(s) and cast some spell over some poor old monster. Now it’s the time to go a step further into the development of our mini system and add another useful piece of gameplay: Items!
With items you can  cure altered status and/or restore health and mana points. As you may guess, we are going to model Items just as usual, through records:

import EBS.Target
import EBS.Status
import Control.Applicative

data Item = Item{itemName :: String,
				         itemDesc :: String,
                 itemEffect :: ItemEffect} deriving (Show)

data ItemEffect = Restore HitPoints ManaPoints
                | Cure [Status] deriving (Show)

--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) -> let newStatus = filter (\s -> s `notElem` rList) <$> (status t)
                                      in case newStatus of
                                              (Just []) -> t {status = Nothing}
                                              _ -> t {status = newStatus}

--Some Items
potion = Item "Potion" "Restores 100 HP" (Restore 100 0)
ether = Item "Ether" "Restores 100 MP" (Restore 0 100)
antidote = Item "Antidote" "Cures Poison status" (Cure [Poison])

Ok, so what I have done here? Simply I’ve created a new type, an Item, with name, description and the item effect, i.e. what happens when an item is used upon a TargetableUnit. As you can see, an Item can restore a certain amount of HitPoints or ManaPoints as well as cure some altered status. Let’s take a look to our use function:

--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) -> let newStatus = filter (\s -> s `notElem` rList) <$> (status t)
                                      in case newStatus of
                                              (Just []) -> t {status = Nothing}
                                              _ -> t {status = newStatus}

It takes an Item, a TargetableUnit and returns a new TargetableUnit, that will be healed or cured from some status. The most interesting part of this function is the second branch of the first case expression, when we manage the case (Cure rList): Essentially we have a TargetableUnit that can be or not affected by some status (I remember you that status is modeled as status :: Maybe [Status]) but we don’t know a priori if that unit have some altered status or not. We’ll use applicative functors to smartly get rid of cured status: we’re filtering the list of status leaving only those status who are not present into the rList (the list who contains the status cured by that item). We use the fmap alias, <$>, to put our function into the Maybe context: we’ll obtain either the new status list, or Nothing is the unit was not affacted by any status (therefore there wasn’t any status to cure). One special case: if we cure all the unit’s status we’ll obtain an empty list, but we want a Nothing value instead, so we have to handle this case properly (see the last case expression).

Gimme some modifier

One flaw of our Battle System was that every spell dealt always the same amount of damage, ignoring inter-element weakness or strengths.  This new version of the cast spell handles this: if some unit is weak against some element, the correspondent spell will deal double damage, converse applies, with only half damage dealt to a unit strong against some element:

--cast function
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 -> case (status t) of
                                     (Just sList) -> t {status = Just $ nub (sList ++ statList)}
                                     Nothing -> t {status = Just statList}

--the damage multiplier function
--Ugly, can I do better?
getDmgMult :: TargetableUnit -> Maybe Element -> Double
getDmgMult t e = case t `isWeakTo` e of
                      Just True -> 2.0
                      Just False -> case t `isStrongAgainst` e of
                                         Just True -> 0.5
                                         Just False -> 1.0
                                         _ -> 1.0
                      _ -> 1.0

Probably I haven’t grasp all the advanced Haskell concept yet, because this function look a little ugly and too pattern matching dependent, but I couldn’t do better. In order to make this works we need to modify our “isWeakTo” and “isStrongAgainst” function in order to accept a Maybe Element: it quite makes sense if you think about it, because no Element translates into a Nothing value:

--If the monster hasn't got any element, the result will be Nothing.
isWeakTo :: TargetableUnit -> Maybe Element -> Maybe Bool
m `isWeakTo` elem = case elem of
                         Just e -> checkProperty m ((==e) . succ)
                         _ -> Nothing

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

Ok! So we have monsters, we have some spells and some items, we can damage monster or even heal them (such a fool action!).
Let’s give this code a try:

ghci> :l EBS/Main.hs
[1 of 7] Compiling EBS.Status       ( EBS/Status.hs, interpreted )
[2 of 7] Compiling EBS.Elemental    ( EBS/Elemental.hs, interpreted )
[3 of 7] Compiling EBS.Target       ( EBS/Target.hs, interpreted )
[4 of 7] Compiling EBS.Item         ( EBS/Item.hs, interpreted )
[5 of 7] Compiling EBS.Spell        ( EBS/Spell.hs, interpreted )
[6 of 7] Compiling EBS.MonsterPark  ( EBS/MonsterPark.hs, interpreted )
[7 of 7] Compiling EBS.Main         ( EBS/Main.hs, interpreted )
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, mp = 50, elemType = Just Fire, status = Nothing}
ghci> let cursed_piros = cast frogSong piros
ghci> cursed_piros
Unit {name = "Piros", level = 1, hp = 300, mp = 50, elemType = Just Fire, status = Just [Frog,Sleep]}
ghci> use remedy cursed_piros
Unit {name = "Piros", level = 1, hp = 300, mp = 50, elemType = Just Fire, status = Just [Frog]}
--Wake up, Piros!
ghci> cast earth piros
Unit {name = "Piros", level = 1, hp = 0, mp = 50, elemType = Just Fire, status = Nothing}
--The original piros bind, double damage! (Earth deal 150 dmg, 300 to piros since it's weak to Earth)
ghci> cast fire piros
Unit {name = "Piros", level = 1, hp = 200, mp = 50, elemType = Just Fire, status = Nothing}
--Normal damage, 100 hp.

Now we miss only the most important thing: a Player!
Stay tuned!

Ah, I’ve opened yet another git repo here, so you can download the fully working code! Just run ghci into the parent directory and import the module in this way:

:l EBS/Main.hs

Enjoy!

6 thoughts on “Let’s make an Elemental Battle System in Haskell – Part III

  1. You can use the Maybe monad instance in getDmgMult and ‘maybe’ function to resolve Nothing.
    Something like:
    getDmgMult :: TargetableUnit -> Maybe Element -> Double
    getDmgMult t e = maybe 1 id $ do
    c <- t `isWeakTo` e
    c' <- t `isStrongAgainst` e
    if c then return 2 else
    if c' then return 0.5 else
    mzero

    I don't know if there is a way to avoid the nested if

  2. you may want to look into the ‘maybe’ function

    Prelude> :t maybe
    maybe :: b -> (a -> b) -> Maybe a -> b

    also, if you find yourself unwrapping maybes and then wrapping them back up, you may want to consider using the Maybe monad:

    isStrongAgainst :: TargetableUnit -> Maybe Element -> Maybe Bool
    m `isStrongAgainst` elem = do e <- elem
    checkProperty m ((==e) . pred)

  3. In first place, thanks to everyone for the precious comments. If only I had known that posting my articles on Reddit could generate so much views! 😀
    Btw the code changes and improves day by day, as much as my Haskell knowledge. I will take your comments as gold and I will make some elegant refactoring in the next post!
    Thanks!
    Alfredo

  4. About the nested if you can use function guard.

    *Main Control.Monad> :t guard
    guard :: MonadPlus m => Bool -> m ()
    *Main Control.Monad> guard True :: Maybe ()
    Just ()
    *Main Control.Monad> guard False :: Maybe ()
    Nothing

    This project the Bool in the Maybe monad.
    Then you can alternate the branch with mplus again for the Maybe monad

    Main Control.Monad> :t mplus
    mplus :: MonadPlus m => m a -> m a -> m a
    *Main Control.Monad> Just 1 `mplus` Nothing
    Just 1
    *Main Control.Monad> Just 1 `mplus` Just 2
    Just 1
    *Main Control.Monad> Nothing `mplus` Just 2
    Just 2

    So here is the code with getDmgMult eating isWeakTo and isStrongTo factorized in function check

    — import Control.Monad

    getDmgMult t elem = maybe 1 id $ do
    e >= guard
    (check succ >> return 2) `mplus` (check pred >> return 0.5)

    The Maybe is also instance of Alternative and Applicative so
    mplus == () and
    x >> return y == y <$ x

    — import Control.Applicative
    — import Control.Monad

    getDmgMult t elem = maybe 1 id $ do
    e >= guard
    2 <$ check succ 0.5 <$ check pred

    I hope I didn't break the code logic in all this.

  5. Nah , posting code in the messages is not clever, it deletes parts, sorry.
    Complete code is here


    import Control.Monad
    import Control.Applicative
    checkProperty = undefined
    getDmgMult t elem = maybe 1 id $ do
    e <- elem
    let check f = checkProperty t ((==e) . f) >>= guard
    (check succ >> return 2) `mplus` (check pred >> return 0.5)
    getDmgMult t elem = maybe 1 id $ do
    e <- elem
    let check f = checkProperty t ((==e) . f) >>= guard
    2 <$ check succ <|> 0.5 <$ check pred

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...