F# Card Game Part 2 - Dealing Cards

In part one of this series we walked through modeling the domain of our card game. In this post we'll use that domain as a guide to help us create a deck, shuffle the deck, and deal cards from the deck.

Creating the deck

There may be more elegant ways to do this, but for now we'll simply define a value and populate it with the cards.

let newDeck =  
    [(Two, Spades); (Three, Spades); (Four, Spades); (Five, Spades); 
    (Six, Spades); (Seven, Spades); (Eight, Spades); (Nine, Spades); 
    (Ten, Spades); (Jack, Spades); (Queen, Spades); (King, Spades); (Ace, Spades);
    (Two, Hearts); (Three, Hearts); (Four, Hearts); (Five, Hearts); 
    (Six, Hearts); (Seven, Hearts); (Eight, Hearts); (Nine, Hearts); 
    (Ten, Hearts); (Jack, Hearts); (Queen, Hearts); (King, Hearts); (Ace, Hearts);
    (Two, Clubs); (Three, Clubs); (Four, Clubs); (Five, Clubs); 
    (Six, Clubs); (Seven, Clubs); (Eight, Clubs); (Nine, Clubs); 
    (Ten, Clubs); (Jack, Clubs); (Queen, Clubs); (King, Clubs); (Ace, Clubs);
    (Two, Diamonds); (Three, Diamonds); (Four, Diamonds); (Five, Diamonds); 
    (Six, Diamonds); (Seven, Diamonds); (Eight, Diamonds); (Nine, Diamonds); 
    (Ten, Diamonds); (Jack, Diamonds); (Queen, Diamonds); (King, Diamonds); (Ace, Diamonds)]   

Because this value is immutable (the default in F#), we don't have to make a function that returns a new Card list. No function can modify our newDeck value in any way; it will always be exactly the same value.

Notice I'm continuing to use the term "value" rather than "variable". These values cannot vary once they are assigned. This concept is paramount to the entire series.

Shuffling the deck

Now that we have the concept of a full deck we need to introduce some way to shuffle the deck to produce a random order of cards so that each game is different. Of course, our newDeck value is immutable so what we'll actually do is return a new Card list that is shuffled rather than actually modifying the newDeck value.

let shuffle deck =  
    let random = new System.Random()
    deck |> List.sortBy (fun card -> random.Next())

If we hover over shuffle we'll see that it is a function that takes a 'a list and returns an 'a list. The default for F#'s type inference is to be generic as possible. In the anonymous function we passed into List.sortBy we didn't use the card argument at all so we now have a function that could actually be used to randomly sort any kind of list.

I'm aware that System.Random() is probably not the best choice for generating "true randomness" but that's not the point of this post.

To test it out we can easily run it in our REPL

> newDeck |> shuffle;;

val it : (Rank * Suit) list =  
  [(King, Spades); (Four, Clubs); (Ace, Clubs); (Four, Hearts); (Ten, Clubs);
...

And for proof that no values were harmed during the shuffle process you could evaluate newDeck again and see that it is still the same value it was before.

> newDeck;;

val it : (Rank * Suit) list =  
  [(Two, Spades); (Three, Spades); (Four, Spades); (Five, Spades);

Dealing a card

Now that we can create a shuffled deck we need some way of dealing a card. If our shuffled deck were a mutable list like a javascript array we would probably call pop which would return the first element in the array and mutate the array so that the returned element is no longer part of the array.

The immutable way of dealing a card is to do what's known as a head::tail pattern match. We destructure the list into a head, which is the first element of the list, and a tail which is the rest of the list. That allows us to easily return the drawn card and a new list of cards that can represent the remaining deck.

let deal deck =  
    match deck with
    | head::tail -> (head, tail)
    | [] -> //handle empty list

In any match statement F# forces you to be complete; you must explicitly handle every case that can exist. If I hadn't handled the empty list scenario the compiler would tell me that I had an incomplete pattern match and suggest cases that I hadn't handled.

In the case that we have a non empty list our implementation is quite easy. We return a tuple of the top card and the remaining cards. But what should we do in the case of an empty list?

A functional approach to handling the error state would be to introduce the Option type.

let deal deck =  
    match deck with
    | head::tail -> (Some head, tail)
    | [] -> (None, [])

Looking at the deal function's signature we can see that it takes an 'a list and returns a tuple of an 'a option and an 'a list. This is honest code. It says, I'll try to return you the top card, but I can't guarantee it because you could hand me an empty list.

> newDeck |> shuffle |> deal;;

val it : (Rank * Suit) option * (Rank * Suit) list =  
  (Some (Seven, Clubs),
   [(Two, Hearts); (King, Spades); (Five, Clubs); (Eight, Spades); 
   ...

52 card pick up

An easy way to prove that our deal function does what we'd expect is to draw all the cards in the deck. In a functional sense what that means is that we want to use a recursive function to loop through the deck.

let rec dealAllCards deck =  
    let (card, remainingDeck) = deck |> deal
    match card with 
    | None -> printfn "Cards out"
    | Some c -> 
        printfn "%A" c
        remainingDeck |> dealAllCards

The rec directive tells F# that the function is a recursive function and is allowed to call itself. The name of our function is dealAllCards and it takes one argument which is a Card list

In the first line let (card, remainingDeck) = deck |> deal we're destructuring the tuple returned from our deal function into a card value and a remainingDeck value.

Because our card value is an option type our next step is to match on it to determine our next step.

If our deal function was not able to return a card (the None case) we print "Cards out" to the console.

If our deal function returned a card (the Some c case) we print the card to the console and then recursively call the function with the remainingDeck value.

Running our function in the REPL proves our code works

> newDeck |> shuffle |> dealAllCards;;

(Five, Diamonds)
(Six, Hearts)
(Six, Clubs)
...
Cards out  

Related Posts

Proudly published with Ghost