FCards - Solitaire

16. Tabling/Placing cards

Now that we can see the cards let’s give the player some actions.

Initial play loop

Let’s allow the player to draw some cards out of the deck, and put a card on a stack:

So now our print screen includes the commands and the tabled cards:

============ Solitaire =============
| 1 |  2 |  3 |  4 |  5 |  6 |
[##] [##] [##] [##] [##] [♠9]
[##] [##] [##] [##] [♣Q]
[##] [##] [##] [♠2]
[##] [##] [♥8]
[##] [♦K]
[♦4]
--space--
... Maximum is 
...  5 face-down 
...  + a full count of 13 face-up
...  + 1 space before the table's printed
...  = 19 lines between top of stack and table
--space--
Table: [[[♦5]
Deck : [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[##]
<d>raw cards, <1-6> put on stack, <q>uit

More than one thing

We now can do more than one thing. As this game gets more complicated we’re going to want to keep track of what those things are.

So let’s be intentional about that, and create a DU for the commands the player has available:

type SolitaireCommands = 
  | DrawCards
  | TableToStack of int

And while we’re at it, this should be the only interface the world of players (and coders) use to interact with the game. We don’t want any back doors here!

let applyCommand (cmd: SolitaireCommands) (game: Game) =
  match cmd with 
  | DrawCards -> game |> drawCards
  | TableToStack a -> game // coming up below!

let updateGame game keystroke =
  match keystroke with 
  | 'd' -> game |> applyCommand DrawCards
  | _ -> game

Matching a stack number

In order to match the input of the stack we not only have to check that the character typed is a valid number, but also that it is in the range of 1 to 6.

F# has a very useful tool called Active Patterns to help with the first part:

An Active Pattern is a dynamic pattern matching tool that either returns None for a non-match, or Some(a) for a match that is also parsed for us.

The following example takes a Char and uses the library function GetNumericValue to parse the char. A -1.0 indicates that the value isn’t a number, and so returns None. A successful conversion is forced into being an int (a whole number) and returned as a Some

let (|Number|_|) (ch:Char) =
  match Char.GetNumericValue(ch) with
  | -1.0 -> None
  | a -> a |> int |> Some

We can use the above active pattern to make our updateGame function:

let applyCommand (cmd: SolitaireCommands) (game: Game) =
  match cmd with 
  | DrawCards -> game |> drawCards
  | TableToStack a when (a >= 1 && a <= 6) -> game |> tableToStack (a - 1)
  | _ -> game

let updateGame game keystroke =
  match keystroke with 
  | 'd' -> game |> applyCommand DrawCards
  | Number a -> game |> applyCommand (TableToStack a)
  | _ -> game

Note how we can specify a range in our matcher using the when keyword.

Exercise

Include the extra parts of the printScreen function, and don’t forget to move the cursor up to the top of the screen.

TIP: You may need to clear a line that has gotten shorter. Here’s a value to print to do that:

let clearLine = "\x1B[K"

Also, write the drawCards and tableToStack functions to update the Game object.

See an answer for printing

  let printHeader game =
    printfn "============ Solitaire ============="
    game

  let printStacks game = 
    printfn "%s| 1  |  2  |  3  |  4  |  5  |  6  |" clearLine
    [0..19] |> List.iter (fun cardNum ->
      [0..5] |> List.map (fun stackNum ->
        if game.stacks[stackNum].Length > cardNum then 
          game.stacks[stackNum][cardNum]
          |> sprintf "[%O]"
        else
          // the stack is out of cards
            "     "         
      )
      |> fun strings -> String.Join (" ", strings)
      |> printfn "%s%s" clearLine
    )
    game //pass it on to the next function
  
  let printTable game =
    let tableLine = 
      match game.table with 
      | []  -> ""
      | [a] -> game.table.Head.ToString()
      | more -> 
        String.init game.table.Length (fun _ -> "[")
        + game.table.Head.ToString()
    printfn "\nTable: %s]" tableLine
    game

  let printDeck game =
    String.init game.deck.Length (fun _ -> "[") 
      |> printfn "Deck:  %s###]"
    game

  let printCommands game =
    printfn "<d>raw cards, <1-6> put on stack, <q>uit"
    game

  let printMoveToTop game =
    let maxCardInAnyStack = 
      game.stacks 
      |> List.map (fun stack -> stack.Length )
      |> List.max
    let n = 
      1 //header
      + 1 //stack numbers
      + 21 //stacks
      + 1 //table
      + 1 //deck
      + 1 //commands
      + 1 //current line
    moveUpLines n
    game

  let printScreen game = 
    game 
    |> printMoveToTop
    |> printHeader
    |> printStacks
    |> printTable
    |> printDeck
    |> printCommands

See an answer for updating the game

let drawCards game =
  let withEnoughCardsToDraw =
    match game.deck.Length with
    | n when n < 3 -> 
      // create a new game that's just like the old one
      //  but with the following differences.
      //  (really useful if you have a lot of parts but only want to change a couple)
      {game with  
        deck = game.deck @ game.table
        table = []
      }
    | _ -> game
  // in case there is less than 3 remaining
  let cardsToTake = Math.Min(3, withEnoughCardsToDraw.deck.Length)  
  {withEnoughCardsToDraw with
    table = 
      (withEnoughCardsToDraw.deck |> List.take cardsToTake)
      @ withEnoughCardsToDraw.table //(new cards on top)
    deck = withEnoughCardsToDraw.deck |> List.skip cardsToTake
  }


// a helper to add a card to a numbered stack
let addToStack (stackNum:int) (card:Card) (stacks: StackCard list list) =
  let updatedStack = stacks[stackNum] @ [ stackCard true card ]
  stacks |> List.updateAt stackNum updatedStack

let tableToStack stackNum game =
  match game.table with 
  | [] -> game // do nothing
  | [a] -> 
    {game with 
      table = []; 
      stacks = game.stacks |> addToStack stackNum a 
    }
  | a::rest -> 
    {game with 
      table = rest; 
      stacks = game.stacks |> addToStack stackNum a 
    }

Code so far

#load "./ch13_core.fsx"
open Ch13_core.Core

module Solitaire =
  open System

  type StackCard = {
    card: Card
    isFaceUp: bool
  } with 
      override this.ToString() =
        if this.isFaceUp then
          this.card.ToString()
        else 
          "###"

  type Game = {
    deck: Card list
    table: Card list
    stacks: StackCard list list
  }

  let deal shuffledDeck = 
    let emptyGame = {
      deck = shuffledDeck
      table = []
      stacks = []
    }
    [6..-1..1] 
    |>  List.fold (fun game i -> 
          let newStack = 
            game.deck 
            |> List.take i                        // flip the last card
            |> List.mapi (fun n card -> { isFaceUp = (n = i - 1); card=card}) 
          {
            stacks = game.stacks @ [ newStack ]
            deck = game.deck |> List.skip i
            table = []
          }
        
        ) emptyGame

  let clearLine = "\x1B[K"

  let printHeader game =
    printfn "%s============ Solitaire =============" clearLine
    game

  let printStacks game = 
    printfn "%s| 1  |  2  |  3  |  4  |  5  |  6  |" clearLine
    [0..19] |> List.iter (fun cardNum ->
      [0..5] |> List.map (fun stackNum ->
        if game.stacks[stackNum].Length > cardNum then 
          game.stacks[stackNum][cardNum]
          |> sprintf "[%O]"
        else
          // the stack is out of cards
            "     "         
      )
      |> fun strings -> String.Join (" ", strings)
      |> printfn "%s%s" clearLine
    )
    game //pass it on to the next function
  
  let printTable game =
    let tableLine = 
      match game.table with 
      | []  -> ""
      | a -> 
        String.init a.Length (fun _ -> "[")
        + a.Head.ToString()
        + "]"
    printfn "%s" clearLine //spacer
    printfn "%sTable: %s" clearLine tableLine
    game

  let printDeck game =
    let deckLine = String.init game.deck.Length (fun _ -> "[") 
    printfn "%sDeck:  %s###]" clearLine deckLine
    game

  let printCommands game =
    printfn "%s<d>raw cards, <1-6> put on stack, <q>uit" clearLine
    game

  let printMoveToTop game =
    let n = 
      1 //header
      + 1 //stack numbers
      + 21 //stacks
      + 1 //table
      + 1 //deck
      + 1 //commands
      + 1 //current line
    moveUpLines n
    game

  let printScreen game = 
    game 
    |> printMoveToTop
    |> printHeader
    |> printStacks
    |> printTable
    |> printDeck
    |> printCommands


  let (|Number|_|) (ch:Char) =
    match Char.GetNumericValue(ch) with
    | -1.0 -> None
    | a -> a |> int |> Some
  
  let drawCards game =
    let withEnoughCardsToDraw =
      match game.deck.Length with
      | n when n < 3 -> 
        // create a new game that's just like the old one
        //  but with the following differences.
        //  (really useful if you have a lot of parts but only want to change a couple)
        {game with  
          deck = game.deck @ game.table
          table = []
        }
      | _ -> game
    // in case there is less than 3 remaining
    let cardsToTake = Math.Min(3, withEnoughCardsToDraw.deck.Length)  
    {withEnoughCardsToDraw with
      table = 
        (withEnoughCardsToDraw.deck |> List.take cardsToTake)
        @ withEnoughCardsToDraw.table //(new cards on top)
      deck = withEnoughCardsToDraw.deck |> List.skip cardsToTake
    }
  
  
  // a helper to add a card to a numbered stack
  let addToStack (stackNum:int) (card:Card) (stacks: StackCard list list) =
    let updatedStack = stacks[stackNum] @ [ { isFaceUp=true; card=card} ]
    stacks |> List.updateAt stackNum updatedStack
  
  let tableToStack stackNum game =
    match game.table with 
    | [] -> game // do nothing
    | [a] -> 
      {game with 
        table = []; 
        stacks = game.stacks |> addToStack stackNum a 
      }
    | a::rest -> 
      {game with 
        table = rest; 
        stacks = game.stacks |> addToStack stackNum a 
      }

  type SolitaireCommands = 
    | DrawCards
    | TableToStack of int

  let applyCommand (cmd: SolitaireCommands) (game: Game) =
    match cmd with 
    | DrawCards -> game |> drawCards
    | TableToStack a when (a >= 1 && a <= 6) -> game |> tableToStack (a - 1)
    | _ -> game

  let updateGame game keystroke =
    match keystroke with 
    | 'd' -> game |> applyCommand DrawCards
    | Number a -> game |> applyCommand (TableToStack a)
    | _ -> game
;;
// DO IT!
let play() =
  newDeck 
  |> shuffle 
  |> Solitaire.deal 
  |> loopGame Solitaire.printScreen Solitaire.updateGame