To Curry in League of Legends

2020-07-04

One of the most well-known and popular online multi-player games is League of Legends. In this game two teams of five players compete against other. Within the game each player controls a champion that they can use to defeat the enemy team by destroying their base. There are many aspects required to winning a game of League of Legends: good in-game strategy, good control of champions but also selecting a good composition of 5 champions, out of more than a hundred, that fits the needs of the players and play-styles.

One of the lesser-known, but nonetheless very innovative, programming languages is Curry. What makes this language so unique that it is a functional logic language. This means that it combines features of functional programming, composing a program through functions, and logic programming, defining the program as predicates of a logic system and searching for answers.

This article aims to give an introduction to the Curry language by creating a small system in it that matches and recommends League Of Legends champions based on player preferences.

If you want to code along with this article, one can use SMAP to evaluate Curry code online, aside from installing a Curry language implementation such as PACKS locally. The code described in this article can be found in a source repository in its entirety as well.

In order to build such a recommendation system, the first thing we need to do is figure out a way to represent both the player preferences and the League of Legends champions in Curry.

To start off, let's define the aspects of a champion in Curry as follows:

module Main where

data Tag = Fighter | Tank | Marksman | Assassin | Mage | Support
         deriving (Show, Eq)
                                       
data Champion =
     Champion {name :: String,
              attack :: Int,
              defense :: Int,
              magic :: Int,
              difficulty :: Int,
              tags :: []Tag}
     deriving (Show, Eq)

Lets just go over the above code step-by-step.

The first line is the module declaration, which is essentially a grouping of related code together. We will define all elements required for our champion recommendation program in this Main module.

Next we define the data types for our representation of Champion Tags and Champions. Curry is a statically typed language, meaning you can define the structure of the data as types, and you can check these types at compile time before the actual program is run. For example, it only allows the following Tags to be used for the Champions: Fighter, Tank, Marksman, Assassin, Mage, Support. Similarly, a Champion is a record of very specific keys and types of values. For example it has a field for name that must be a string of characters, a field for attack that must be an integer number and a field for tags which must be a list of Tags that we previously defined.

These types allow us to define various champions avaiable in League of Legends. For example, the champion Aatrox can be defined as:

aatrox :: Champion
aatrox = Champion {name = "Aatrox",
                       attack = 8,
                       defense = 4,
                       magic = 3,
                       difficulty = 4,
                       tags = [Fighter, Tank]
                       }

Note that this is a correct champion as it fulfills every criteria we gave to the type of Champion. If we would define this champion in the wrong way, such as:

aatrox :: Champion
aatrox = Champion {name = "Aatrox",
                       attack = "high",
                       defense = 4,
                       magic = 3,
                       difficulty = 4,
                       tags = [Fighter, Tank]
                       }

the compiler of Curry would give us an error, "Type error in record construction", along with some additional information that the attack field has a value that does not match the type.

In this definition we explicitly denoted that Aatrox is of the type champion using the line:

aatrox :: Champion

This is not a strict requirement. Curry can infer the type simply from the constructor of the record Champion. Curry can perform type inference even in many other more complex cases, but for clarity we will give the types of records and functions whenever we can.

The final part of this definition, is the portion:

     deriving (Show, Eq)

Without going into too much detail, these makes instances of Champions, and Tags, easy to compare and print as a string.

Aatrox, a champion in League of Legends Copyright Riot Games

The above code should be very familiar to Haskell programmers! The syntax, the type system and many other aspects of Curry are heavily influenced by Haskell. Even the naming of the two languages have the same origin: they are both named after the logician Haskell Curry.

Now that we got the basics out of the way let us define a lot more champions than just Aatrox.

aatrox :: Champion
aatrox = Champion {name = "Aatrox",
                       attack = 8,
                       defense = 4,
                       magic = 3,
                       difficulty = 4,
                       tags = [Fighter, Tank]
                       }
darius :: Champion
darius = Champion {name = "Darius",
                   attack = 9,
                   defense = 5,
                   magic = 1,
                   difficulty = 2,
                   tags = [Fighter, Tank]
                  }


fiora :: Champion
fiora = Champion {name = "Fiora",
                  attack = 10,
                  defense = 4,
                  magic = 2,
                  difficulty = 3,
                  tags = [Fighter, Assassin]
                 }

gnar :: Champion
gnar = Champion {name = "Gnar",
                 attack = 6,
                 defense = 5,
                 magic = 5,
                 difficulty = 8,
                 tags = [Fighter, Tank]
                 }

irelia :: Champion
irelia = Champion {name = "Irelia",
                  attack = 7,
                  defense = 4,
                  magic = 5,
                  difficulty = 5,
                  tags = [Fighter, Assassin]
                  }

karma :: Champion
karma = Champion {name = "Karma",
                  attack = 1,
                  defense = 7,
                  magic = 8,
                  difficulty = 5,
                  tags = [Mage, Support]
                  }

maokai :: Champion
maokai = Champion {name = "Maokai",
                   attack = 3,
                   defense = 8,
                   magic = 6,
                   difficulty = 3,
                   tags = [Tank, Mage]
                  }

neeko :: Champion
neeko = Champion {name = "Neeko",
                  attack = 1,
                  defense = 1,
                  magic = 9,
                  difficulty = 5,
                  tags = [Mage, Support]
                  }

sylas :: Champion
sylas = Champion {name = "Sylas",
                  attack = 3,
                  defense = 4,
                  magic = 8,
                  difficulty = 5,
                  tags = [Mage, Assassin]
                  }

vayne :: Champion 
vayne = Champion {name = "Vayne",
                  attack = 10,
                  defense = 1,
                  magic = 1,
                  difficulty = 8,
                  tags = [Marksman, Assassin]
                  }

champions :: [Champion]
champions = [aatrox,
             darius,
             fiora,
             gnar,
             irelia,
             karma,
             maokai,
             neeko,
             sylas,
             vayne
            ]

The above code defines a total of 10 different Champions that exist in League of Legends, whose data values are derived from Riot games Data Dragon API. For easy access, we define a list of champions as the aptly named champions. Note that in Curry a list is denoted by elements between []. The type for a list of elements is denoted by using these bracket in front of the type, such as []Tag, which denotes the type of a list of Tags.

Now that we have the champion definitions out of the way, we also want to define player preferences that we aim to match champions against. Here our preferences consists of a summoner name, identifying the player, minimum requirements for the champion's attack, defense and magic values, a maximum difficulty rating, as well as tags on type of champions the player prefers.

data Preference =
    Preference {summonerName :: String,
                minAttack :: Int,
                minDefense :: Int,
                minMagic :: Int,
                maxDifficulty :: Int,
                prefTags :: []Tag}
     deriving (Show, Eq)

Using this Preference type we define two player preferences, one for Alice and one for Bob, as well as a collection for both of them.

alicePref :: Preference
alicePref = Preference {  summonerName = "Alice",
                          minAttack = 2,
                          minDefense = 1,
                          minMagic = 4,
                          maxDifficulty = 3,
                          prefTags = [Mage]
                         }

bobPref :: Preference
bobPref = Preference {  summonerName = "Bob",
                        minAttack = 2,
                        minDefense = 5,
                        minMagic = 2,
                        maxDifficulty = 1,
                        prefTags = [Tank, Support]}

preferences :: [Preference]
preferences = [ alicePref,
                bobPref
              ]

We have now defined both the champions and the player preferences and we can move towards creating the functionality to match them together. But first a bit of introduction, or a refresher depending on the readers background, to functional programming.

As mentioned at the start of this article, Curry is a language that supports both functional and logical programming. Functional programming allows us to express the program we want to write as a composition of functions.

To use an example, suppose we want to write a program that given a list of champions, returns only those champion that have an attack of more than 5. In order to do this we can first write a function that given a champion returns true if it has an attack more than 5 and returns false if this is not the case. Then we can use this function as filter inside another function, that only returns members of a list of champions that have more than 5 attack.

To put this together, such a function would look as follows:

highAttackChamps :: [Champion] -> [Champion]
highAttackChamps champList = filter (\ champion -> attack champion >5) champList

There are a couple of things to note here.

First is that the function that "returns true only if the attack of a champion is greater than 5" has no name. Such anonymous functions, also known as lambda expressions, can be defined using a form starting with a backslash (\) followed by the parameter names of the function, followed by an arrow (->) and the expressions that this function should execute. In this case this expression first gets the attack of the champion and compares it, to give the right result. Anonymous functions are very convenient for expressions that we do not aim to reuse somewhere else.

The second thing to note is the function composition that allows the filtering. The filter function is a predefined function that is imported by default, as a part of a group of functions such as these called a Prelude. As mentioned before this takes a function as a parameter that returns a Bool (True or False), as well as a list, and returns only those members of the list for which the function returned true. Using a function as an argument makes the filter function very versatile. If we, for example wanted to filter based on champion defense, we could have just replaced function used as an argument. Such a composition using functions is one of the core elements of what makes functional programming so effective.

The final thing to note is how the highAttackChamps is given a type. The type declaration highAttackChamps :: [Champion] -> [Champion] shows that this function takes a list of champions and returns a list of champions, where the arrow -> functions as a separator between the type of the parameter(s) and/or the type of returned value.

Now that we got over the basics of functional programming lets start to put this knowledge to good use by writing a number of functions that helps us match the Preferences with the Champions. These functions are designed to match a single criteria within a Preference with the value of a Champion. If this match succeeds that the function returns True, if it fails it returns a False. We have a function to match based on attack, defense, magic and difficulty.

matchChampionAttack :: Preference -> Champion -> Bool
matchChampionAttack pref champ = (minAttack pref) <= (attack champ) 

matchChampionDefense :: Preference -> Champion -> Bool
matchChampionDefense pref champ = (minDefense pref) <= (defense champ) 

matchChampionMagic :: Preference -> Champion -> Bool
matchChampionMagic pref champ = (minMagic pref) <= (magic champ) 

matchChampionDifficulty :: Preference -> Champion -> Bool
matchChampionDifficulty pref champ = (maxDifficulty pref) >= (difficulty champ) 

Note that the type definition now have three elements separated by an arrow (->), as these functions have two parameters, a Preference and a Champion, and return a Bool (the type for True or False).

For matching the preferences, we are going to explore some of the logic programming features of Curry. In logic programming the program can be defined as a set of facts, rules and (logic) variables, where the underlying logic system can search for a suitable solution given the criteria.

Give given an example, suppose we are looking for Tag that is both part of the preferred tags of the Preferences as well as part of the tags of the Champions. We can state this problem exactly like this in Curry as follows:

matchChampionTag :: Preference -> Champion -> Bool
matchChampionTag pref champ = (elem tag (prefTags pref) && elem tag (tags champ)) =:= True where tag free

To go over the elements in this code (elem tag (prefTags pref) && elem tag (tags champ)) express that tag has to be an element of both the prefTags of the a given preference and tags of the given champion. Unlike with our purely functional code above, we do not give the value of tag as a parameter. Instead we say that this variable is a free variable and let Curry find the values of Tag which makes the above constraint hold True (i.e. the =:= True where tag free portion of the function).

Now that we have all the functions for matching individual elements (i.e.: attack, defense, magic, difficulty and tags) of champions to profiles, we can create combined functions such as matchChampionAny that holds true if any of the elements match and matchChampionAll for which all elements need to match.

matchChampionAny :: Preference -> Champion -> Bool
matchChampionAny pref champ =  matchChampionAttack pref champ ||
                               matchChampionDefense pref champ ||
                               matchChampionMagic pref champ ||
                               matchChampionDifficulty pref champ ||
                               matchChampionTag pref champ


matchChampionAll :: Preference -> Champion -> Bool
matchChampionAll pref champ =  matchChampionAttack pref champ &&
                                 matchChampionDefense pref champ &&
                                 matchChampionMagic pref champ &&
                                 matchChampionDifficulty pref champ &&
                                 matchChampionTag pref champ

One of the coolest features of this language, which we can see in the above definition, is the way logic programming elements can mix with functional elements. In both matchChampionAny and matchChampionAll we use functions that make use of functional programming, as well as logic programming, seamlessly.

Once we got our functions for matching we can use these to match, we finally have all the ingredients to create recommendations. We are going to create two recommendation functions: one for matching based on matchChampionAny which is a "weak" criteria as any there are lot of opportunities to match and one based on matchChampionAll which is a lot "stronger" as all stats of a champion has to match with the preferences.

The functions for these is given as follows:

recommendWeak :: Preference -> [Champion] -> [String]
recommendWeak preference champList = map (\ champion -> name champion) (filter (\ champion -> matchChampionAny preference champion ) champList)

recommendStrong :: Preference -> [Champion] -> [String]
recommendStrong preference champList = map (\champion -> name champion) (filter (\ champion -> matchChampionAll preference champion ) champList)

The constructs in here should look familiar with one exception: the use of map (\champion -> name champion). The map function is similar to filter in takes a function for a parameter as well as a list. Instead of filtering, in instead applies that function to all members of that list. The anonymous function used takes a champion and returns only the name value of that champion. This allows for the matching function to return not a list of champions, but a list of strings i.e.: [String] that are the names of the champions. This makes the return value much less verbose, as we can identify champions by name.

Finally, in order to show off the code detailed here with, we put together some examples of using the all the functions we defined. We then print the answers out, line by line for each of these examples. The main function, with the return type IO (), allows us to not to return a specific value but print the values out, to the REPL or the command-line, depending on how we use our code.

example1 :: [Champion]
example1 = highAttackChamps champions

example2 :: Bool
example2 = matchChampionAttack alicePref aatrox

example3 :: Bool
example3 = matchChampionDifficulty bobPref vayne 

example4 :: Bool
example4 =  matchChampionTag alicePref karma

example5 :: Bool
example5 =  matchChampionAny alicePref neeko

example6 :: Bool
example6 =  matchChampionAll bobPref gnar

example7 :: [String]
example7 =  recommendWeak alicePref champions

example8 :: [String]
example8 =  recommendWeak bobPref champions

example9 :: [String]
example9 =  recommendStrong alicePref champions

example10 :: [String]
example10 =  recommendStrong bobPref champions

main :: IO ()
main = putStrLn ("Example 1, champions with more than 5 attack:" ++ "\n" ++
                 (show example1) ++ "\n" ++
                 "Example 2, matching Aatrox with Alice's preferences based on attack:" ++ "\n" ++
                 (show example2) ++ "\n" ++
                 "Example 3, matching Vayne with Bob's preferences based on difficulty:" ++ "\n" ++
                 (show example3) ++ "\n" ++
                 "Example 4, matching Karma with Alice's preferences based on tag(s):" ++ "\n" ++
                 (show example4) ++ "\n" ++
                 "Example 5, matching Neeko with Alice's preferences based on any matching criteria:" ++ "\n" ++
                 (show example5) ++ "\n" ++
                 "Example 6, matching Gnar with Bob's preferences based on all matching criteria:" ++ "\n" ++
                 (show example6) ++ "\n" ++
                 "Example 7, recommendation based on a weak criteria for Alice:" ++ "\n" ++
                 (show example7) ++ "\n" ++
                 "Example 8, recommendation based on a weak criteria for Bob:" ++ "\n" ++
                 (show example8) ++ "\n" ++
                 "Example 9, recommendation based on a strong criteria for Alice:" ++ "\n" ++
                 (show example9) ++ "\n" ++
                 "Example 10, recommendation based on a strong criteria for Bob:" ++ "\n" ++
                 (show example10))

This results in the following being printed out:

Example 1, champions with more than 5 attack:
[Champion {name = "Aatrox", attack = 8, defense = 4, magic = 3, difficulty = 4, tags = [Fighter,Tank]},Champion {name = "Darius", attack = 9, defense = 5, magic = 1, difficulty = 2, tags = [Fighter,Tank]},Champion {name = "Fiora", attack = 10, defense = 4, magic = 2, difficulty = 3, tags = [Fighter,Assassin]},Champion {name = "Gnar", attack = 6, defense = 5, magic = 5, difficulty = 8, tags = [Fighter,Tank]},Champion {name = "Irelia", attack = 7, defense = 4, magic = 5, difficulty = 5, tags = [Fighter,Assassin]},Champion {name = "Vayne", attack = 10, defense = 1, magic = 1, difficulty = 8, tags = [Marksman,Assassin]}]
Example 2, matching Aatrox with Alice's preferences based on attack:
True
Example 3, matching Vayne with Bob's preferences based on difficulty:
False
Example 4, matching Karma with Alice's preferences based on tag(s):
True
Example 5, matching Neeko with Alice's preferences based on any matching criteria:
True
Example 6, matching Gnar with Bob's preferences based on all matching criteria:
False
Example 7, recommendation based on a weak criteria for Alice:
["Aatrox","Darius","Fiora","Gnar","Irelia","Karma","Maokai","Neeko","Sylas","Vayne"]
Example 8, recommendation based on a weak criteria for Bob:
["Aatrox","Darius","Fiora","Gnar","Irelia","Karma","Maokai","Neeko","Sylas","Vayne"]
Example 9, recommendation based on a strong criteria for Alice:
["Maokai"]
Example 10, recommendation based on a strong criteria for Bob:
[]

Hopefully the result nothing is too surprising and it matches all expectations. The result also verifies some of our assumptions: the weak criteria matches everything in this case, but a strong criteria has some nice results: Maokai is well suited for Alice, and Bob probably needs to widen his criteria, given the above 10 champions.

There are many ways we could proceed from this point on. As one could see from the examples the recommendation based on a weak criteria is quite weak, as anything matches, while a strong criteria can be too restrictive at times. Coming up with the different types of matching functions, could be a next step, if one wants to expand upon this small application. With multiple matching functions one can also write a function that takes a matching function as a parameter in order to avoid writing duplicate code for each recommendation function.

Hopefully this article has given you a bit of an intro to the Curry language and why it is so interesting. There exists a number of options for using functional and logic programming separately but this is one of the few languages that aims to combine both. If you wish to explore either paradigms further I wish you the best of luck to curry on with your endeavors.