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
Interesting way to get into Haskell 🙂
Have you checked functors and applicatives? You could refactor weakTo and strongTo
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
tmp.hs
hosted with ❤ by GitHub
🙂
Thanks, I’m happy that you liked this series (already took a look to part 2 and 3?)
My Haskell knowledge improves day by day thanks to a wonderful book called “Learn You a Haskell for a Great Good”, so this code will be better and better (I hope 🙂 )
Yea, I’ve already studied functors and applicative, but I wanna refactor my code with the more powerful (and awesome) Monads! 😀
Stay tuned!
Alfredo
Pingback: Let’s make an Elemental Type System in Haskell – Part I « Another Word For It