Now that we can see the cards let’s give the player some actions.
Let’s allow the player to draw some cards out of the deck, and put a card on a stack:
deck
, place them on the table
, and display the top one only to the playertable
onto a selected stack
. This reveals the next card in the table
until there are no more on the table
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
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
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, orSome(a)
for a match that is also parsed for us.The following example takes a
Char
and uses the library functionGetNumericValue
to parse the char. A -1.0 indicates that the value isn’t a number, and so returnsNone
. A successful conversion is forced into being anint
(a whole number) and returned as aSome
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.
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.
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
}
#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