Let’s make an Elemental Type System in Haskell – Part II

Ok, so we have our elements, our monster but we can do nothing more instancing some monster and checking for their weakness. We need to think about some design choices and how they could affect our program. The main drawback working with Haskell Records is the namespace pollution: you can’t define two distinct record with one or more fields names in common because Haskell will give you an error. This happens because Haskell under the hood converts that fields into global functions, so we can access our monster property just typing:

name myMonster

Pretty cool, but unfortunately we couldn’t do the same if we have defined another type, e.g. Player, with the same field.

Is everything lost? No! There are several workaround on the net, but thinking about our game I’ve discovered that isn’t a big problem to use a single type in order to model both a monster and a player: both have a name, hp, mp, level, and even a status:

data TargetableUnit = Unit{name :: String,
                           level :: Int,
                           hp :: HitPoints,
                           mp :: ManaPoints,
                           elemType :: Maybe Element,
                           status :: Maybe [Status]} deriving (Eq, Read, Show)

And what about elemType? “A Player can’t have an element!” you may say, but think about that: suppose you have just obtained a brand new ring (e.g. Tetra Elemental), that absorb one or more elemental damage.. with the elemeType field you can model this behaviour! Well, in this first version you can have only ONE Element, but changing Maybe Element in Maybe [Element] seems pretty straightforward. Ok, so we have our TargetableUnit, it’s the time to create some spells!

It’s a kind of magic

In order to have magic, we need to have spells, modeled as a type:

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

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

--cast function
cast :: Spell -> TargetableUnit -> TargetableUnit
cast s t =
    case spellEffect s of
       Damage hit mana -> t {hp = hp t - hit, mp = mp t - mana}
       Inflict statList -> case (status t) of
                                (Just sList) -> t {status = Just (sList ++ statList)}
                                Nothing -> t {status = Just statList}

--SPELL DEFINITIONS
fire   = Spell "Fire"   20 (Just Fire) (Damage 100 0)
fira   = Spell "Fira"   40 (Just Fire) (Damage 200 0)
firaga = Spell "Firaga" 80 (Just Fire) (Damage 300 0)

bio = Spell "Bio" 20 Nothing (Inflict [Poison])
frogSong = Spell "Frog Song" 30 Nothing (Inflict [Frog, Sleep])

Ok, so what we have done? Essentially a magic does two thing:

  1. Deal damage
  2. Inflict one or more negative status
  3. Both

In this version just ignore the third case, we’ll work on that later. Now watch the SpellEffect definition: HitPoints and ManaPoints are simply two type synonym (they are Integer), and the type is pretty straightforward, isn’t it? A SpellEffect can be resolved or with Damage or with a status infliction. The Spell type reflect this, with the spellEffect leaved as last field. Nothing to say about the cast function: picks a spell, a TargetableUnit (so this function will be the same for a monster and for the player) and return a new unit (remember, no side effect allowed) with less hp/mp (in case of Damage) or with more status in case of Inflict.

Let’s play with our new system:

ghci> :l EBS/Main.hs
[1 of 5] Compiling EBS.Status       ( EBS/Status.hs, interpreted )
[2 of 5] Compiling EBS.Elemental    ( EBS/Elemental.hs, interpreted )
[3 of 5] Compiling EBS.Target       ( EBS/Target.hs, interpreted )
[4 of 5] Compiling EBS.Spell        ( EBS/Spell.hs, interpreted )
[5 of 5] Compiling EBS.Main         ( EBS/Main.hs, interpreted )
Ok, modules loaded: EBS.Main, EBS.Elemental, EBS.Target, EBS.Status, EBS.Spell.
ghci> let piros = Unit "Piros" 1 100 50 (Just Fire) Nothing
ghci> piros
Unit {name = "Piros", level = 1, hp = 100, mp = 50, elemType = Just Fire, status = Nothing}
ghci> cast bio piros
Unit {name = "Piros", level = 1, hp = 100, mp = 50, elemType = Just Fire, status = Just [Poison]}
ghci> let newMonster = cast bio piros
ghci> newMonster
Unit {name = "Piros", level = 1, hp = 100, mp = 50, elemType = Just Fire, status = Just [Poison]}
ghci> cast frogSong newMonster
Unit {name = "Piros", level = 1, hp = 100, mp = 50, elemType = Just Fire, status = Just [Poison,Frog,Sleep]}

Pretty cool, isn’t it? We are far from have a usable or fun mini game, but now we can cast spell on a monster!

I’ll create a git repo ASAP, so you will find the entire code there!

Stay tuned!

Alfredo

Annunci

Let’s make an Elemental Type System in Haskell – Part I

Hello everyone,

recently I falled in love (again) with Haskell, and I’ve decided to start a simple toy project just to stretch my Haskell muscles fibers.
Meanwhile, I’ve started Final Fantasy VII again, so I thought to realize a simple Elemental Type System. I dunno how much this project will be complex, but a lot of fun awaits.

The elemental cycle

The first to do is to choose an Elemental cycle i.e. how elements influence each other. I’ve choose this simple schema (don’t blame me for the source):

For now just ignore the star in the center of the picture, for us “Fire is weak against Earth, strong against Lightning”. Ok, so we need a type representing our elements, and we need a circular one, because we want be free to invoke

pred

and

succ

without having our program to crash because we reach a boundary. Unfortunately, I haven’t found a better way to implement this without having to manually enum each element:

--We want a cyrcular enumeration for our elems
--so we can't rely on the Bounded type
data Element = Fire | Earth | Wind | Water | Lightning
  deriving (Eq, Ord, Show, Read)

instance Enum Element where
    toEnum 0 = Fire
    toEnum 1 = Earth
    toEnum 2 = Wind
    toEnum 3 = Water
    toEnum 4 = Lightning
    toEnum i = toEnum $ i `mod` 5

    fromEnum Fire = 0
    fromEnum Earth = 1
    fromEnum Wind = 2
    fromEnum Water = 3
    fromEnum Lightning = 4

    enumFromTo x y = map toEnum [a .. b']
      where a = fromEnum x
            b = fromEnum y
            b' = if a <= b then b else b + 5

    enumFromThen x1 x2 = error "enumFromThen not supported for Element"
    enumFromThenTo x1 x2 y = error "enumFromThenTo not supported for Element"

We haven’t done much yet, fun starts right on! The second step is create a monster type, through the record syntax, which allow to nicely get the type properties:

data Monster = Monster {name :: String,
                        hp :: Integer,
                        mp :: Integer,
                        elemType :: Maybe Element}
                        deriving (Eq, Read)

instance Show Monster where
    show m = "Name: " ++ name m ++ "\n" ++
             "HP: " ++ show (hp m) ++ "\n" ++
             "MP: " ++ show (mp m) ++ "\n" ++
             "Element: " ++ show (elemType m)

I’ve created an instance of Show for the type Monster for pretty printing purpose. Note how elemType is of type “Maybe Element”, just because not every monster is an elemental one! Using Maybe we can make our elemental system more flexible and less crash prone. The next step is to create four simple functions to play with monsters weakness and strenghts:

weakTo :: Monster -> Maybe Element
weakTo m = case elemType m of
                Just e -> Just $ succ e
                _ -> Nothing

strongAgainst :: Monster -> Maybe Element
strongAgainst m = case elemType m of
                       Just e -> Just $ pred e
                       _ -> Nothing

isWeakTo :: Monster -> Element -> Bool
m `isWeakTo` elem = case elemType m of
                         Just e -> elem == (succ e)
                         _ -> False

isStrongAgainst :: Monster -> Element -> Bool
m `isStrongAgainst` elem = case elemType m of
                                Just e -> elem == (pred e)
                                _ -> False

As you can see, the code is pretty straightforward. We can use our functions just like a sort of “Sense” spell, searching for monster weakness (I remember you that a spell can be more or less effective depending on the type of the target). This functions works but as you can see there is too much boilerplate code, since we are changing only a function (pred and succ) between the two. Function application operator comes in rescue:

checkProperty :: Monster -> (Element -> Element) -> Maybe Element
checkProperty m f = case elemType m of
                    Just e -> Just . f $ e
                    _ -> Nothing

checkProperty is our general purpose function who checks for monster weakness and strengths, and here are the new functions versions:

weakTo' :: Monster -> Maybe Element
weakTo' m = checkProperty m succ

strongAgainst' :: Monster -> Maybe Element
strongAgainst' m = checkProperty m pred

Wow, only one line of code! The dirty job is done by “checkProperty”, and you can write similar and shorten function even for isWeakTo and isStrongAgainst

Let’s test them!

Now the funniest part, let’s have some fun with ghci:

:l Types.hs
[1 of 1] Compiling Main             ( Types.hs, interpreted )
Ok, modules loaded: Main.
ghci> let piro = Monster{name = "Piro", hp = 70, mp = 200, elemType = Just Fire}
ghci> checkProperty piro succ
Just Earth
ghci> checkProperty piro pred
Just Lightning
ghci> :r
[1 of 1] Compiling Main             ( Types.hs, interpreted )
Ok, modules loaded: Main.
ghci> let piro = Monster{name = "Piro", hp = 70, mp = 200, elemType = Just Fire}
ghci> strongAgainst' piro
Just Lightning
ghci> weakTo' piro
Just Earth
ghci> let dragon = Monster{name = "Dragon", hp = 200, mp = 100, elemType = Nothing}
ghci> dragon `isWeakTo` Fire
False
ghci> piro `isWeakTo` Earth
True
ghci>dragon
Name: Dragon
HP: 200
MP: 100
Element: Nothing
ghci>piro
Name: Piro
HP: 70
MP: 200
Element: Just Fire

Stay tuned for other improvement! Have fun with Haskell!

Alfredo