Tutorial for Love

While Love is designed to be a compilation target, it has its own syntax which will be used in this tutorial. We will implement in this tutorial a simple version of the Mastermind.

This a 2-player game, the Masterind and the Challenger. The Mastermind chooses secretly a sequence of five colors that the Challenger must guess. For every guess the Challenger makes, the Mastermind tells the challenger the number of colors well placed and the number of colors that are in the combination, but that are not well placed.

Types

First of all, we will define the different types that will structure the contract data.

type color = Red | Blue | Green | Yellow | Orange

Color is a sum type - it defines a set of constructors representing the different colors. Here, five colors are defined.

type combination = color * color * color

A combination is a tuple of colors in a given order. In our example, there are three colors in a combination (for example, (Blue, Red, Green) is a combination).

type player = address

A player is simply defined by its address, which represents the unique identifier of an account or a contract.

type result = {
  well_placed : nat;
  not_well_placed : nat;
  wrong : nat
}

Once a combination is submitted, the mastermind will need to tell the player how many colors are well placed, not well placed or wrong. Its answer will have the form of a record, defining different named fields. A value r of type result will hold three counters well_placed, not_well_placed and wrong that can respectively be accessed with r.well_placed, r.not_well_placed and r.wrong. These counters have the type nat, or positive integers.

type mastermind = {
  player : player;
  answer : combination
}

The type mastermind also is a record, holding the mastermind address and the correct answer, its secret combination.

type 'a seat = Empty | Filled of 'a

Before any player starts the game, the contract will wait for a mastermind and a challenger to take a seat. The type ‘a seat is a polymorphic type, i.e. it depends on a type argument ‘a. For example, int seat represents the type with two constructors Empty and Filled of int.

Fun fact : the type ‘a option - a Love built-in type - defines the similar constructors None and Some of ‘a.

type storage = {
  mastermind : mastermind seat;
  challenger : player seat;
  guess_list : (combination * result) list;
}

The storage type represents the data structure saved by the contract. It contains the seat of the mastermind, the seat of the player and the list of combininations with their corresponding result, in which will be saved after each guess of the challenger.

Contract values

Now the data structure of the contract is defined, we will start to design the game rules. First of all, the initial storage of contract will represents two empty seats - one for the mastermind and one for the player - and an empty guess list.

val empty_table : storage =
  let mastermind_seat = Empty [:mastermind] in
  let challenger_seat = Empty [:player] in
  let guess_list = [] [:combination * result] in {
     mastermind = mastermind_seat;
     challenger = challenger_seat;
     guess_list;
   }

The empty_table value of type storage is defined by the mastermind seat and the challenger seat, which are both empty. Empty constructors are followed by type applications that allows to determine what is its type. While they are both Empty, they cannot be compared as their type is strictly different : the first is a mastermind seat, the second is a player seat. At last the empty guess list, represented by the empty list constructor [] and its type application [:combination * result] With these elements, the storage record can be built.

NB : Note that the field guess_list is not explicitely assigned. This is because there is already a variable with the exact same name, which will be choosen by default. It is impossible to partially define records.

We have now defined a value of type storage. This value, as well as every Love value, is persistent: it cannot be modified. It is however possible to build new values from these.

val add_challenger (challenger : player) (storage : storage) : storage =
  {storage with challenger = Filled challenger [:player]}

The value add_challenger defines a function with two arguments: challenger of type player and storage of type storage. It returns a value of type storage. The value returned is a copy of the value in argument where we modified the challenger field to fill it.

val add_mastermind
    (master : player)
    (answer : combination)
    (storage : storage) : storage =
  let mastermind = {player = master; answer} in
  { storage with mastermind = Filled mastermind [:mastermind] }

More generally, every function argument always have the form (VAR_NAME : TYPE) and, as for empty_table, the list of arguments is followed by the type of the body.

val is_valid (c : combination) : bool =
  c.0 <> [:color] c.1 &&
  c.1 <> [:color] c.2 &&
  c.2 <> [:color] c.0

A mastermind combination is valid if and only if all its colors are different. The function is_valid requires an argument c of type combination, and will return a boolean value. The function is_valid checks that every color is distinct. The distinct operator <> is a built-in construction expecting the type of the elements being compared. As we compare colors, we apply to <> the type color as we did for constructors when we defined empty_table.

val in_combination (color : color) (combination : combination) : bool =
  color =[:color] combination.0 ||
  color =[:color] combination.1 ||
  color =[:color] combination.2

Similarly, the function in_combination tests that a given color belong to a combination.

val%private empty_result : result = {
  well_placed = 0p;
  not_well_placed = 0p;
  wrong = 0p
}

We now define utilities for the result type. First, an empty_resuly that will be used for storing the result of a guess. Each field is initially equal to 0p, the zero of naturals.

Such a result has no meaning outside the contract; we can hide it from other contracts with the token %private.

val well_placed (s : result) : result =
  { s with well_placed = s.well_placed ++ 1p }

val not_well_placed (s : result) : result =
  { s with not_well_placed = s.not_well_placed ++ 1p }

val wrong (s : result) : result =
  { s with wrong = s.wrong ++ 1p }

val is_winning (result : result) : bool =
  result.well_placed = [:nat] 3p

The functions well_placed, not_well_placed and wrong increments the different counters of the result in argument. The addition on naturals have its own addition operator ++. You can find more informations on primitives on the reference manual.

Reminder: Love values are immutable. Each new record is a copy of the argument in which we updated a field.

val which_are_correct (guess : combination) (answer : combination) : result =
  let test_color
        (guess : color)
        (answer_color : color)
        (result : result) =
    if guess = [:color] answer_color
    then well_placed result
    else if in_combination guess answer
    then not_well_placed result
    else result
  in
  let result = empty_result in
  let result = test_color guess.0 answer.0 result in
  let result = test_color guess.1 answer.1 result in
  let result = test_color guess.2 answer.2 result in
  result

The function which_are_correct will compare the player’s guess and the mastermind’s answer. For this purpose, it defined a function test_color that will test if a color is well placed or not.

NB: The return type of test_color is not required, as it can easily be guessed.

val mastermind_bet : dun = 15.0DUN
val challenger_bet : dun = 15.0DUN
val guess_cost     : dun = 5.0DUN

When the mastermind and the challenger want to play, they will have to bet a certain amount of duns. Each guess will cost the player 5 duns, directly sent to the mastermind. When the challenger guesses the right combination, it gets back its initial bet and the masterminds.

Contract initializer, entry points and views

For players and masterminds to interact with the contract, we will define an initializer, multiple entrypoints and views.

Initializer

When a Love contract is originated, an initial value must be provided. If the contract has an initializer function, its result will be the storage of the contract.

Love initializers have strict definitions: they must start with val%init, be named storage and take exactly one typed argument. It must always build a element of type storage, hence the return type of the initializer must not be provided.

val%init storage (_ : unit) = empty_table

In our case, the initializer argument is _. This pattern meaning that we don’t want to give the argument a name as it is not used in the initializer’s body. However, it must be given a value of type unit to be called. The only value of type unit is ().

NB: If the contract has no initializer, the initial value given during the origination will be the initial storage of the contract.

Entry points

It is possible to interact with contracts through entry points. These values are the only elements of the contract that can alter the contract storage.

An entry point starts with val%entry followed by its name and three arguments: the storage, the amount sent in the transaction and the transaction parameter. Only this last parameter is explicitely given a type as the first argument always have the type storage and the second always have the type dun.

Entry points return an operation list and a value of type storage. The returned storage value will replace the current contract storage, and then perform the operations of the list.

val%entry challenge storage bet (_ : unit) =
  if bet <> [:dun] challenger_bet
  then failwith [:string * dun] ("Bad challenger bet", bet) [:unit]
  else ();

  let () =
    match storage.challenger with
      Empty -> ()
    | Filled a -> failwith [:string * player] ("There is already a challenger", a) [:unit]
  in

  let new_storage =
    let sender = Current.sender () in
    add_challenger sender storage
  in
  ([][:operation], new_storage)

The challenge entry point will register the person who initiated the transaction as the mastermind of the current game. It first performs several checks: is the bet correct? Is there already a challenger?

  if bet <> [:dun] challenger_bet
  then failwith [:string * dun] ("Bad challenger bet", bet) [:unit]
  else ();

The failwith instruction is a built-in Love primitive that raises an exception. It expects in this specific order the content type of the exception, the content of the exception and the return type. You can find more details on failwith on the reference manual.

This expression has no effect in the else case, hence it has type unit. Such expressions can be separated by ; so that they are calculated, and then ignored (but when an exception is raised and not catched, the execution will stop).

  let () =
    match storage.challenger with
      Empty -> ()
    | Filled a -> failwith [:string * player] ("There is already a challenger", a) [:unit]
  in

If there already is a challenger, the contract must fail. This expression matches the storage challenger to check if it is empty or filled. In the first case it does nothing, otherwise it raises an exception.

  let new_storage =
    let sender = Current.sender () in
    add_challenger sender storage
  in
  ([][:operation], new_storage)

If everything goes well, then the sender of the transaction is set as the challenger of the game in a copy of the storage, which is returned as the new storage of the contract.

val%entry be_mastermind storage bet (answer : combination) =
  if bet <> [:dun] mastermind_bet
  then failwith [:string * dun] ("Bad mastermind bet", bet) [:unit];

  if not (is_valid answer)
  then
    failwith
      [:string * combination]
      ("Answer is invalid: every color must be different", answer)
      [:unit];

  let () =
    match storage.mastermind with
      Empty -> ()
    | Filled m ->
        failwith [:string * player] ("There is already a mastermind", m.player) [:unit]
  in

  let new_storage =
    let sender = Current.sender () in
    add_mastermind sender answer storage
  in
  ([][:operation], new_storage)

The be_mastermind entrypoint is similar to the challenger for registering the mastermind, but it requires a combination that is set as the game answer.

NB: if the else branch of a condition is not explicited, it is interpreted as else ()

val%entry guess storage bet (guess : combination) =
  if bet <> [:dun] guess_cost
  then failwith [:string * dun] ("To guess, you must may 5dun", bet) [:unit];

  if not (is_valid guess)
  then
    failwith
      [:string * combination]
      ("Guess is invalid: every color must be different", guess)
      [:unit];

  let player = Current.sender () in
  let mastermind =
    match storage.mastermind with
      Empty -> failwith [:string] ("There is no mastermind") [:mastermind]
    | Filled m -> m
  in
  let () =
    match storage.challenger with
      Empty -> failwith [:string] ("You must challenge the mastermind before playing") [:unit]
    | Filled a -> (
        if a <>[:player] player
	then failwith [:string * player] ("You are not the challenger", a) [:unit]
      )
  in

  let result = which_are_correct mastermind.answer guess in
  let operation, new_storage =
    if is_winning result
    then ( (* Send everything on the contract to the player *)
      let op =
        let contract_funds = Current.balance () in
        Account.transfer player contract_funds in
      let new_storage = empty_table in
      op, new_storage
    ) else ( (* Send the player's bet to the mastermind *)
      let op = Account.transfer mastermind.player bet in
      let new_storage =
        let new_guess_list = (guess, result) :: storage.guess_list in
        {storage with  guess_list = new_guess_list}
      in
      op, new_storage
    )
  in [operation][:operation], new_storage

Finally, the guess entrypoint compares the combination parameter to the mastermind answer. The challenger must pay 5dun to guess the correct answer. If the `combination is the correct one, then all the contract funds are sent to the challenger and the storage is reinitialized. Otherwise, the contract pays 5DUN to the mastermind.

Views

Other contracts on the chain can read the public content of a contract, but its storage is private. The only way to give a read-only access to a contract storage is by defining views.

Views are functions are defined by two arguments: the storage and a parameter. As for entry points, only the parameter is explicitely typed as the first argument always have the type storage. However, the return type of a view must be specified

val%view guesses storage (_ : unit) : (combination * result) list = storage.guess_list

This view gives an access to other contracts to the list of guesses of the current challenger. It may be useful if the challenger itself is a contract implementing an mastermind AI for example.

Many other things

This minimalist mastermind version was a short introduction to the strength of the Love language.

In the next tutorials, we will see how to hide the storage from the challenger by hashing the answer, how to create contracts that interact as a simple challenger AI, and much more !

If you are curious about the strength of Love, you can learn about modules, signatures, contract creation and polymorphism on the reference manual.