To play a game, we can expand to a standard play loop:
So that means within our play loop we need to include a function that updates the game according to the command.
let moveUpLines n = printfn "\x1B[%dA" n //moves the cursor up "n" lines
let printScreen game =
printfn "===FCARDS==="
printfn "%O" game
printfn "<p>ickup card <q>uit"
moveUpLines 5
let combineUpdaterAndPrinter updater game command =
let updated = updater game command
printScreen updated
updated
let loopGame (updater: Game -> char -> Game) (initialGame: Game) =
printScreen initialGame
(fun _ -> Console.ReadKey().KeyChar |> Char.ToLowerInvariant)
|> Seq.initInfinite
|> Seq.takeWhile (fun x -> x <> 'q')
|> Seq.fold (combineUpdaterAndPrinter updater) initialGame
One the last line we use a new standard function called
fold
. This loops through the collection of things (in this case the keystrokes) and applies them to an accumulator (in our case it’s the initial Game), but in each step in the loop it uses the updated Game from the previous step. It’s a way of doing the following but with a collection of things that you may not know the value of yet (e.g. the player’s chosen keystrokes):game |> updater 'p' |> updater 'p' |> updater 'r' |> updater 'a' |> updater '?' |> updater 'q' //yay we quit!
Also, on that last line, we called the function combineUpdaterAndPrinter
. But the function has three inputs and we only provided one - what’s going on?!?
Functions in F# are pretty cool. They actually only take one input at a time and then generate a sub-function for taking the next input, and so on, and so on.
So, the function “
let combineUpdaterAndPrinter updater game command = ...
” is actuallylet combineUpdaterAndPrinter updater = let sub_function1 game = let sub_function2 command = ...
That means when we only provided the first updater input, it actually returned a function that takes a game ( and command ), which just happens to be the exact right shape for the
fold
operation. In this way we can pre-bake a function into a desired shape with already setup values.TIP: To take advantage of this partial application of functions, the order of the inputs is important. You can only pre-bake from left to right. Generally, this means that we list the inputs that change the least (or are considered configuration?) first, and the things that change the most (or get passed along to the next function in the chain of pipes?) go to the right.
Extra TIP for experts: Functions that aren’t built in F# (i.e. probably built in C#) don’t use partial application, and instead have a single tuple of inputs.
You may have noticed this when we used the standard function
String.Join()
in chapter 10. It takes a tuple of a separator and an array of strings
- So it’s
String.Join: (string * string array) -> string
called asString.Join(", ", values)
- Not as
String.Join: string -> string array -> string
, called asString.Join ", " values
Design the updater
function. It takes the parameters of a Game
and a Char
and returns a newly updated Game
.
We want it to pickup a card only if the player presses ‘p’, otherwise return the Game unchanged.
let updateGame game command =
match command with
| 'p' -> game |> pickupCard
| _ -> game
Now we can play a simple game of “pick up the cards”!
let play() =
let startingpoint =
{
deck = newDeck |> shuffle
hand = []
}
loopGame updateGame startingpoint
open System
//COLOR CODES
let COLOR_DEFAULT = "\x1B[0m"
let COLOR_RED = "\x1B[91m"
let COLOR_BLACK = "\x1B[90m"
type CardNumber =
| Two
| Three
| Four
| Five
| Six
| Seven
| Eight
| Nine
| Ten
| Jack
| Queen
| King
| Ace
with
override this.ToString() =
match this with
| Two -> "2 "
| Three -> "3 "
| Four -> "4 "
| Five -> "5 "
| Six -> "6 "
| Seven -> "7 "
| Eight -> "8 "
| Nine -> "9 "
| Ten -> "10"
| Jack -> "J "
| Queen -> "Q "
| King -> "K "
| Ace -> "A "
type Card =
| Hearts of CardNumber
| Diamonds of CardNumber
| Clubs of CardNumber
| Spades of CardNumber
| Joker
with
override this.ToString() =
match this with
| Hearts x -> $"{COLOR_RED}\u2665{x}{COLOR_DEFAULT}"
| Diamonds x -> $"{COLOR_RED}\u2666{x}{COLOR_DEFAULT}"
| Clubs x -> $"{COLOR_BLACK}\u2663{x}{COLOR_DEFAULT}"
| Spades x -> $"{COLOR_BLACK}\u2660{x}{COLOR_DEFAULT}"
| Joker -> "Jok"
let printOut (hand: 'a seq) =
"[" + String.Join("] [", hand) + "]"
let newDeck =
let suits = [Hearts; Diamonds; Clubs; Spades]
let numbers = [
Two; Three; Four; Five; Six; Seven; Eight; Nine; Ten;
Jack; Queen; King; Ace
]
List.allPairs suits numbers
|> List.map (fun (suit, number) -> suit number)
type Game = {
deck: Card list
hand: Card list
} with
override this.ToString() =
$"[###] - {this.deck.Length}\n" + (printOut this.hand)
let pickupCard (game: Game) =
match game.deck with
| [] -> failwith "No cards left!!!"
| [a] -> { hand = game.hand @ [a]; deck = [] }
| a::rest -> { hand = game.hand @ [a]; deck = rest }
let rec shuffle deck =
let random = System.Random()
match deck with
| [] -> []
| [a] -> [a]
| _ -> // NOTE: `_` means "some value, but I don't care what it is"
let randomPosition = random.Next(deck.Length)
let cardAtPosition = deck[randomPosition]
let rest = deck |> List.removeAt randomPosition
[cardAtPosition] @ (shuffle rest)
let moveUpLines n = printfn "\x1B[%dA" n //moves the cursor up "n" lines
let printScreen game =
printfn "===FCARDS==="
printfn "%O" game
printfn "<p>ickup card <q>uit"
moveUpLines 5
let combineUpdaterAndPrinter updater game command=
let updated = updater game command
printScreen updated
updated
let loopGame (updater: Game -> char -> Game) (initialGame: Game) =
printScreen initialGame
(fun _ -> Console.ReadKey().KeyChar |> Char.ToLowerInvariant)
|> Seq.initInfinite
|> Seq.takeWhile (fun x -> x <> 'q')
|> Seq.fold (combineUpdaterAndPrinter updater) initialGame
let updateGame game command =
match command with
| 'p' -> game |> pickupCard
| _ -> game
let play() =
let startingpoint =
{
deck = newDeck |> shuffle
hand = []
}
loopGame updateGame startingpoint
;;
// DO IT!
play()