Haskell Program to Play the Blackjack Card Game
For much of my work using Haskell I deal mostly with pure code with smaller bits of impure code for network and file IO, etc. Realizing that my use case for using Haskell (mostly pure code) may not be typical, I wanted the last example “cookbook recipe” in this book to be an example dealing with changing state, a program to play the Blackjack card game.
The game state is maintained in the type Table that holds information on a randomized deck of cards, the number of players in addition to the game user and the card dealer, the cards in the current hand, and the number of betting chips that all players own. Table data is immutable so all of the major game playing functions take a table and any other required inputs, and generate a new table as the function result.
This example starts by asking how many players, besides the card dealer and the game user, should play a simulated Blackjack game. The game user controls when they want another card while the dealer and any other simulated players play automatically (they always hit when their card score is less than 17).
I define the types for playing cards and an entire card deck in the file Card.hs:
1 -- Card model: defines `Rank`, `Suit`, and `Card`, with helpers
2 -- `orderedCardDeck` builds a deterministic deck; `cardValue` maps ranks to scores
3 module Card (Card, Rank, Suit, orderedCardDeck, cardValue) where
4
5 import Data.Maybe (fromMaybe)
6 import Data.List (elemIndex)
7 import Data.Map (fromList, lookup, keys)
8
9 data Card = Card { rank :: Rank
10 , suit :: Suit }
11 deriving (Eq, Show)
12
13 data Suit = Hearts | Diamonds | Clubs | Spades
14 deriving (Eq, Show, Enum, Ord)
15
16 data Rank = Two | Three | Four
17 | Five | Six | Seven | Eight
18 | Nine | Ten | Jack | Queen | King | Ace
19 deriving (Eq, Show, Enum, Ord)
20
21 rankMap = fromList [(Two,2), (Three,3), (Four,4), (Five,5),
22 (Six,6), (Seven,7), (Eight,8), (Nine,9),
23 (Ten,10), (Jack,10), (Queen,10),
24 (King,10), (Ace,11)]
25
26 -- Deterministic deck: list-comprehension over all ranks and a suit range
27 -- Adjust the suit range if you want to include all suits (Enum ranges)
28 orderedCardDeck :: [Card]
29 orderedCardDeck = [Card rank suit | rank <- keys rankMap,
30 suit <- [Hearts .. Clubs]]
31
32 cardValue :: Card -> Int
33 cardValue aCard =
34 case (Data.Map.lookup (rank aCard) rankMap) of
35 Just n -> n
36 Nothing -> 0 -- should never happen
This module defines essential components for representing and working with playing cards.
Data Types
-
Card: A record type with two fields:rank :: Rank- Represents the card’s rank (e.g., Two, Queen, Ace).suit :: Suit- Represents the card’s suit (e.g., Hearts, Spades).
Suit: An enumeration defining the four card suits: Hearts, Diamonds, Clubs, Spades. It derivesEq(equality),Show(string representation),Enum(enumeration capabilities), andOrd(ordering) for convenience.Rank: An enumeration listing the thirteen card ranks, from Two to Ace. It also derivesEq,Show,Enum, andOrd.
Functions and Values
rankMap: AData.Mapthat associates eachRankwith its corresponding numerical value in games like Blackjack.orderedCardDeck: A list comprehension that generates a standard 52-card deck, sorted by rank within each suit.cardValue: A function that takes aCardand returns its numerical value based on therankMap. It uses pattern matching to handle theMaybetype returned byData.Map.lookup.
Explanation
CardData Type: The core of the module. It defines a playing card as a combination of aRankand aSuit.SuitandRankEnumerations: These provide a clear and type-safe representation of suits and ranks. DerivingEnumandOrdallows easy iteration and comparison.rankMap: This map is crucial for assigning numerical values to cards, particularly in games where card values matter (e.g., Blackjack).orderedCardDeck: This function generates a standard 52-card deck. It uses list comprehension to iterate over allRankvalues (obtained from the keys ofrankMap) and allSuitvalues (fromHeartstoClubs), creating aCardfor each combination.cardValue: This function retrieves the numerical value of a given card. It usesData.Map.lookupto find the value associated with the card’s rank inrankMap. Thecaseexpression handles the possibility oflookupreturningNothing(which should ideally never happen in this context).
Key Points
- The code provides a well-structured representation of playing cards in Haskell.
- The use of enumerations enhances type safety and readability.
Data.Mapis employed for efficient lookup of card values.- The
orderedCardDeckfunction conveniently generates a standard deck of cards.
As usual, the best way to understand this code is to go to the GHCi repl:
1 *Main Card RandomizedList Table> :l Card
2 [1 of 1] Compiling Card ( Card.hs, interpreted )
3 Ok, modules loaded: Card.
4 *Card> :t orderedCardDeck
5 orderedCardDeck :: [Card]
6 *Card> orderedCardDeck
7 [Card {rank = Two, suit = Hearts},Card {rank = Two, suit = Diamonds},Card {rank = Two, suit = Clubs},Card {rank = Three, suit = Hearts},Card {rank = Three,
8 ...
9 *Card> head orderedCardDeck
10 Card {rank = Two, suit = Hearts}
11 *Card> cardValue $ head orderedCardDeck
12 2
So, we have a sorted deck of cards and a utility function for returning the numerical value of a card (we always count ace cards as 11 points, deviating from standard Blackjack rules).
The next thing we need to get is randomly shuffled lists. The Haskell Wiki has a good writeup on randomizing list elements and we are borrowing their function randomizedList (you can see the source code in the file RandomizedList.hs). Here is a sample use:
1 *Card> :l RandomizedList.hs
2 [1 of 1] Compiling RandomizedList ( RandomizedList.hs, interpreted )
3 Ok, modules loaded: RandomizedList.
4 *RandomizedList> import Card
5 *RandomizedList Card> randomizedList orderedCardDeck
6 [Card {rank = Queen, suit = Hearts},Card {rank = Six, suit = Diamonds},Card {rank = Five, suit = Clubs},Card {rank = Five, suit = Diamonds},Card {rank = Seven, suit = Clubs},Card {rank = Three, suit = Hearts},Card {rank = Four, suit = Diamonds},Card {rank = Ace, suit = Hearts},
7 ...
Much of the complexity in this example is implemented in Table.hs which defines the type Table and several functions to deal and score hands of dealt cards:
- createNewTable :: Players -> Table. Players is the integer number of other players at the table.
- setPlayerBet :: Int -> Table -> Table. Given a new value to bet and a table, generate a new modified table.
- showTable :: Table -> [Char]. Given a table, generate a string describing the table (in a format useful for development)
- initialDeal :: [Card] -> Table -> Int -> Table. Given a randomized deck of cards, a table, and the number of other players, generate a new table.
- changeChipStack :: Int -> Int -> Table -> Table. Given a player index (index order: user, dealer, and other players), a new number of betting chips for the player, and a table, then generate a new modified table.
- setCardDeck :: [Card] -> Table -> Table. Given a randomized card deck and a table, generate a new table containing the new randomized card list; all other table data is unchanged.
- dealCards :: Table -> [Int] -> Table. Given a table and a list of player indices for players wanting another card, generate a new modified table.
- resetTable :: [Card] -> Table -> Int -> Table. Given a new randomized card deck, a table, and a new number of other players, generate a new table.
- scoreHands :: Table -> Table. Given a table, score all dealt hands and generate a new table with these scores. There is no table type score data, rather, we “score” by changing the number of chips all of the players (inclding the dealer) has.
- dealCardToUser :: Table -> Int -> Table. For the game user, always deal a card. For the dealer and other players, deal another card if their hand score is less than 17.
- handOver :: Table -> Bool. Determine if the current hand is over.
- setPlayerPasses :: Table -> Table. Call this function when the payer passes. Other players and dealer are then played out automatically.
The implementation in the file Table.hs is fairly simple, with the exception of the use of Haskell lenses to access nested data in the table type. I will discuss the use of lenses after the program listing, but: as you are reading the code look out for variables starting with the underscore character _ that alerts the Lens system that it should create data accessors for these variables.
This code defines a module named Table which provides data structures and functions to simulate a simplified table in a card game, potentially Blackjack.
Core Components
-
Tabledata type:-
Represents the state of the table, storing information like:
- Number of players
- Chip stacks for each player
- Cards dealt to each player (including the dealer)
- Current player’s bet
- Whether the user has passed their turn
- The remaining card deck
-
-
Functions:
createNewTable: Creates a new table with the specified number of players and initial chip stacks.resetTable: Resets the table for a new round, clearing dealt cards and optionally changing the card deck.setCardDeck: Sets a new card deck for the table.dealCards: Deals cards to specified players.initialDeal: Performs the initial deal at the beginning of a round.showTable: Generates a string representation of the table’s current state.scoreHands: Calculates and updates chip stacks based on player and dealer scores.setPlayerBet: Sets the current player’s bet.setPlayerPasses: Simulates the player passing their turn, dealing additional cards to other players and the dealer.changeChipStack: Modifies a specific player’s chip stack.score: Calculates the score of a player’s hand.dealCardToUser: Deals a card to a specified player, with special handling for the user and dealer.handOver: Checks if the user has passed their turn.
Lenses
The code uses lenses (makeLenses ''Table) to provide convenient access and modification of the Table data type’s fields.
Game Logic (Simplified)
- The code seems to implement a basic version of a card game where players and the dealer are dealt cards.
scoreHandscalculates scores and updates chip stacks based on win/loss conditions.dealCardToUserhandles dealing cards, ensuring the dealer keeps drawing until their score is at least 17.setPlayerPassessimulates the user passing, triggering the dealer and other players to finish their turns.
1 {-# LANGUAGE TemplateHaskell #-} -- for makeLens
2
3 -- Game state and rules for Blackjack; pure transformations on `Table`
4 -- Uses lenses to update nested fields concisely
5 module Table (Table (..), createNewTable, setPlayerBet, showTable, initialDeal,
6 changeChipStack, setCardDeck, dealCards, resetTable, scoreHands,
7 dealCardToUser, handOver, setPlayerPasses) where -- note: export dealCardToUser only for ghci development
8
9 import Control.Lens
10
11 import Card
12 import Data.Bool
13 import Data.Maybe (fromMaybe)
14
15 data Table = Table { _numPlayers :: Int
16 , _chipStacks :: [Int] -- number of chips, indexed by player index
17 , _dealtCards :: [[Card]] -- dealt cards for user, dealer, and other players
18 , _currentPlayerBet :: Int
19 , _userPasses :: Bool
20 , _cardDeck :: [Card]
21 }
22 deriving (Show)
23
24 type Players = Int
25
26 createNewTable :: Players -> Table
27 createNewTable n =
28 Table n
29 [500 | _ <- [1 .. n]] -- give each player (incuding dealer) 10 chips
30 [[] | _ <- [0..n]] -- dealt cards for user and other players (we don't track dealer's chips)
31 20 -- currentPlayerBet
32 False
33 [] -- placeholder for random shuffled card deck
34
35 resetTable :: [Card] -> Table -> Int -> Table
36 resetTable cardDeck aTable numberOfPlayers =
37 Table numberOfPlayers
38 (_chipStacks aTable)
39 [[] | _ <- [0..numberOfPlayers]]
40 (_currentPlayerBet aTable)
41 False
42 cardDeck
43
44 -- Use lens extensions:
45
46 makeLenses ''Table
47
48 showDealtCards :: [[Card]] -> String
49 showDealtCards dc =
50 (show [map cardValue hand | hand <- dc])
51
52 setCardDeck :: [Card] -> Table -> Table
53 setCardDeck newDeck =
54 over cardDeck (\_ -> newDeck)
55
56 dealCards :: Table -> [Int] -> Table
57 dealCards aTable playerIndices =
58 last $ scanl dealCardToUser aTable playerIndices
59
60 -- Initial deal: reset table with a new shuffled deck, then deal two rounds
61 initialDeal cardDeck aTable numberOfPlayers =
62 dealCards
63 (dealCards (resetTable cardDeck aTable numberOfPlayers) [0 .. numberOfPlayers])
64 [0 .. numberOfPlayers]
65
66 showTable :: Table -> [Char]
67 showTable aTable =
68 "\nCurrent table data:\n" ++
69 " Chipstacks: " ++
70 "\n Player: " ++ (show (head (_chipStacks aTable))) ++
71 "\n Other players: " ++ (show (tail (_chipStacks aTable))) ++
72 "\n User cards: " ++ (show (head (_dealtCards aTable))) ++
73 "\n Dealer cards: " ++ (show ((_dealtCards aTable) !! 1)) ++
74 "\n Other player's cards: " ++ (show (tail (tail(_dealtCards aTable)))) ++
75 -- "\n Dealt cards: " ++ (show (_dealtCards aTable)) ++
76 "\n Dealt card values: " ++ (showDealtCards (_dealtCards aTable)) ++
77 "\n Current player bet: " ++
78 (show (_currentPlayerBet aTable)) ++
79 "\n Player pass: " ++
80 (show (_userPasses aTable)) ++ "\n"
81
82 clipScore aTable playerIndex =
83 let s = score aTable playerIndex in
84 if s < 22 then s else 0
85
86 -- Resolve bets for the hand: compare each player's score against dealer
87 -- Busts are treated as 0; updates chip stacks accordingly
88 scoreHands aTable =
89 let chipStacks2 = _chipStacks aTable
90 playerScore = clipScore aTable 0
91 dealerScore = clipScore aTable 1
92 otherScores = map (clipScore aTable) [2..]
93 newPlayerChipStack = if playerScore > dealerScore then
94 (head chipStacks2) + (_currentPlayerBet aTable)
95 else
96 if playerScore < dealerScore then
97 (head chipStacks2) - (_currentPlayerBet aTable)
98 else (head chipStacks2)
99 newOtherChipsStacks =
100 map (\(x,y) -> if x > dealerScore then
101 y + 20
102 else
103 if x < dealerScore then
104 y - 20
105 else y)
106 (zip otherScores (tail chipStacks2))
107 newChipStacks = newPlayerChipStack:newOtherChipsStacks
108 in
109 over chipStacks (\_ -> newChipStacks) aTable
110
111 setPlayerBet :: Int -> Table -> Table
112 setPlayerBet newBet =
113 over currentPlayerBet (\_ -> newBet)
114
115 setPlayerPasses :: Table -> Table
116 setPlayerPasses aTable =
117 let numPlayers = _numPlayers aTable
118 playerIndices = [1..numPlayers]
119 t1 = over userPasses (\_ -> True) aTable
120 t2 = dealCards t1 playerIndices
121 t3 = dealCards t2 playerIndices
122 t4 = dealCards t3 playerIndices
123 in
124 t4
125
126
127 changeChipStack :: Int -> Int -> Table -> Table
128 changeChipStack playerIndex newValue =
129 over chipStacks (\a -> a & element playerIndex .~ newValue)
130
131 scoreOLD aTable playerIndex =
132 let scores = map cardValue ((_dealtCards aTable) !! playerIndex)
133 totalScore = sum scores in
134 if totalScore < 22 then totalScore else 0
135
136 score aTable playerIndex =
137 let scores = map cardValue ((_dealtCards aTable) !! playerIndex)
138 totalScore = sum scores in
139 totalScore
140
141 dealCardToUser' :: Table -> Int -> Table
142 dealCardToUser' aTable playerIndex =
143 let nextCard = head $ _cardDeck aTable
144 playerCards = nextCard : ((_dealtCards aTable) !! playerIndex)
145 newTable = over cardDeck (\cd -> tail cd) aTable in
146 over dealtCards (\a -> a & element playerIndex .~ playerCards) newTable
147
148 -- Dealer/AI rule: user always draws; other players draw until score >= 17
149 dealCardToUser :: Table -> Int -> Table
150 dealCardToUser aTable playerIndex
151 | playerIndex == 0 = dealCardToUser' aTable playerIndex -- user
152 | otherwise = if (score aTable playerIndex) < 17 then
153 dealCardToUser' aTable playerIndex
154 else aTable
155
156 handOver :: Table -> Bool
157 handOver aTable =
158 _userPasses aTable
In line 46 we use the function makeLenses to generate access functions for the type Table. We will look in some detail at lines 52-54 where we use the lense over function to modify a nested value in a table, returning a new table:
1 setCardDeck :: [Card] -> Table -> Table
2 setCardDeck newDeck =
3 over cardDeck (\_ -> newDeck)
The expression in line 3 evaluates to a partial function that takes another argument, a table, and returns a new table with the card deck modified. Function over expects a function as its second argument. In this example, the inline function ignores the argument it is called with, which would be the old card deck value, and returns the new card deck value which is placed in the table value.
Using lenses can greatly simplify the code to manipulate complex types.
Another place where I am using lenses is in the definition of function scoreHands (lines 88-109). On line 109 we are using the over function to replace the old player betting chip counts with the new value we have just calculated:
1 over chipStacks (\_ -> newChipStacks) aTable
Similarly, we use over in line 113 to change the current player bet. In function handOver on line 157, notice how I am using the generated function _userPasses to extract the value of the user passes boolean flag from a table.
The function main, defined in the file Main.hs, uses the code we have just seen to represent a table and modify a table, is fairly simple. A main game loop repetitively accepts game user input, and calls the appropriate functions to modify the current table, producing a new table. Remember that the table data is immutable: we always generate a new table from the old table when we need to modify it.
1 -- Simple CLI Blackjack runner; orchestrates IO and table updates
2 -- Prompts for players, shuffles deck, loops reading commands to hit/stay/bet
3 module Main where
4
5 import Card -- pure code (card types + values)
6 import Table -- pure code (game state + rules)
7 import RandomizedList -- impure code (random shuffle)
8
9 printTable :: Table -> IO ()
10 printTable aTable =
11 putStrLn $ showTable aTable
12
13 randomDeck =
14 randomizedList orderedCardDeck
15
16 -- Main loop: renders the table, shuffles a fresh deck each turn,
17 -- and processes user input; returns `IO` to keep side effects explicit
18 gameLoop :: Table -> Int -> IO b
19 gameLoop aTable numberOfPlayers = do
20 printTable aTable
21 cardDeck <- randomDeck
22 if (handOver aTable) then
23 do
24 putStrLn "\nHand over. State of table at the end of the game:\n"
25 printTable aTable
26 putStrLn "\nNewly dealt hand:\n"
27 gameLoop (initialDeal cardDeck (scoreHands aTable) numberOfPlayers) numberOfPlayers
28 else
29 do
30 putStrLn "Enter command: h)it or set bet to 10, 20, 30; any other key to stay:"
31 command <- getLine
32 if elem command ["10", "20", "30"] then gameLoop (setPlayerBet (read command) aTable) numberOfPlayers
33 else
34 if command == "h" then gameLoop (dealCards aTable [0 .. numberOfPlayers]) numberOfPlayers
35 else gameLoop (setPlayerPasses (dealCards aTable [1 .. numberOfPlayers])) numberOfPlayers
36 -- player stays (no new cards)
37
38 main :: IO b
39 main = do
40 print "Start a game of Blackjack. Besides yourself, how many other players do you want at the table?"
41 s <- getLine
42 let num = (read s :: Int) + 1 -- player indices: 0)user, 1)dealer, and > 1 are the other players
43 cardDeck <- randomDeck
44 let aTable = initialDeal cardDeck (createNewTable num) num
45 gameLoop aTable num
This module combines the previously defined Card and Table modules with an impure RandomizedList module to implement the main game loop of a simplified Blackjack-like card game.
Core Functions
printTable: Prints the current state of the table using theshowTablefunction from theTablemodule.randomDeck: Generates a randomized version of theorderedCardDeckusing therandomizedListfunction (assumed to be from theRandomizedListmodule).-
gameLoop: The core recursive function that drives the game:- Prints the current table state.
- Generates a random card deck.
- If the hand is over (user has passed), prints the final table state, scores hands, and starts a new game with the updated chip stacks.
-
Otherwise, prompts the user for a command:
- If the command is “10”, “20”, or “30”, sets the player’s bet.
- If the command is “h”, deals cards to all players (including the user).
- If any other command is entered, sets the user as passed and deals cards to the dealer and other players until they stand.
- Recursively calls itself with the updated table state.
-
main:- Prompts the user for the number of additional players.
- Creates a new table with the specified number of players and an initial deal.
- Starts the
gameLoop.
Key Points
- The code demonstrates a basic interactive text-based card game implementation.
- It combines pure modules (
Card,Table) with an impure module (RandomizedList) for randomization. - The
gameLoopfunction handles user input and game state transitions. - The game logic is likely simplified for this example, and a full Blackjack implementation would require additional rules and features.
Remember: The RandomizedList module is assumed to provide a function randomizedList for shuffling the card deck, introducing impurity into the game logic.
I encourage you to try playing the game yourself, but if you don’t here is a sample game:
1 *Main Card RandomizedList Table> main
2 Start a game of Blackjack. Besides yourself, how many other
3 players do you want at the table?
4 1
5
6 Current table data:
7 Chipstacks:
8 Player: 500
9 Other players: [500]
10 User cards: [Card {rank = Three, suit = Clubs},Card {rank = Two, suit = Hearts}]
11 Dealer cards: [Card {rank = Queen, suit = Diamonds},Card {rank = Seven, suit = Clubs}]
12 Other player's cards: [[Card {rank = King, suit = Hearts},Card {rank = Six, suit = Diamonds}]]
13 Dealt card values: [[3,2],[10,7],[10,6]]
14 Current player bet: 20
15 Player pass: False
16
17 Enter command: h)it or set bet to 10, 20, 30; any other key to stay:
18 h
19
20 Current table data:
21 Chipstacks:
22 Player: 500
23 Other players: [500]
24 User cards: [Card {rank = Six, suit = Hearts},Card {rank = Three, suit = Clubs},Card {rank = Two, suit = Hearts}]
25 Dealer cards: [Card {rank = Queen, suit = Diamonds},Card {rank = Seven, suit = Clubs}]
26 Other player's cards: [[Card {rank = Eight, suit = Hearts},Card {rank = King, suit = Hearts},Card {rank = Six, suit = Diamonds}]]
27 Dealt card values: [[6,3,2],[10,7],[8,10,6]]
28 Current player bet: 20
29 Player pass: False
30
31 Enter command: h)it or set bet to 10, 20, 30; any other key to stay:
32 h
33
34 Current table data:
35 Chipstacks:
36 Player: 500
37 Other players: [500]
38 User cards: [Card {rank = King, suit = Clubs},Card {rank = Six, suit = Hearts},Card {rank = Three, suit = Clubs},Card {rank = Two, suit = Hearts}]
39 Dealer cards: [Card {rank = Queen, suit = Diamonds},Card {rank = Seven, suit = Clubs}]
40 Other player's cards: [[Card {rank = Eight, suit = Hearts},Card {rank = King, suit = Hearts},Card {rank = Six, suit = Diamonds}]]
41 Dealt card values: [[10,6,3,2],[10,7],[8,10,6]]
42 Current player bet: 20
43 Player pass: False
44
45 Enter command: h)it or set bet to 10, 20, 30; any other key to stay:
46
47 Current table data:
48 Chipstacks:
49 Player: 500
50 Other players: [500]
51 User cards: [Card {rank = King, suit = Clubs},Card {rank = Six, suit = Hearts},Card {rank = Three, suit = Clubs},Card {rank = Two, suit = Hearts}]
52 Dealer cards: [Card {rank = Queen, suit = Diamonds},Card {rank = Seven, suit = Clubs}]
53 Other player's cards: [[Card {rank = Eight, suit = Hearts},Card {rank = King, suit = Hearts},Card {rank = Six, suit = Diamonds}]]
54 Dealt card values: [[10,6,3,2],[10,7],[8,10,6]]
55 Current player bet: 20
56 Player pass: True
57
58 Hand over. State of table at the end of the game:
59
60 Current table data:
61 Chipstacks:
62 Player: 520
63 Other players: [520]
64 User cards: [Card {rank = King, suit = Clubs},Card {rank = Six, suit = Hearts},Card {rank = Three, suit = Clubs},Card {rank = Two, suit = Hearts}]
65 Dealer cards: [Card {rank = Queen, suit = Diamonds},Card {rank = Seven, suit = Clubs}]
66 Other player's cards: [[Card {rank = Eight, suit = Hearts},Card {rank = King, suit = Hearts},Card {rank = Six, suit = Diamonds}]]
67 Dealt card values: [[10,6,3,2],[10,7],[8,10,6]]
68 Current player bet: 20
69 Player pass: True
Here the game user has four cards with values of [10,6,3,2] for a winning score of 21. The dealer has [10,7] for a score of 17 and the other player has [8,10,6], a value greater than 21 so the player went “bust.”
I hope that you enjoyed this last example that demonstrates a reasonable approach for managing state when using immutable data.