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 (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, Bounded)
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 -- | Map each 'Rank' to its Blackjack point value.
22 rankMap :: Map Rank Int
23 rankMap = fromList [(Two,2), (Three,3), (Four,4), (Five,5),
24 (Six,6), (Seven,7), (Eight,8), (Nine,9),
25 (Ten,10), (Jack,10), (Queen,10),
26 (King,10), (Ace,11)]
27
28 -- | Deterministic deck: list-comprehension over all ranks and all four suits.
29 -- Uses [minBound .. maxBound] to guarantee every Suit is included.
30 orderedCardDeck :: [Card]
31 orderedCardDeck = [Card r s | r <- keys rankMap,
32 s <- [minBound .. maxBound]]
33
34 -- | Look up the point value of a card's rank.
35 cardValue :: Card -> Int
36 cardValue aCard = fromMaybe 0 (Data.Map.lookup (rank aCard) rankMap)
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 --
6 -- Player indexing convention:
7 -- 0 = human player
8 -- 1 = dealer
9 -- 2 … n = AI players
10 module Table (Table (..), createNewTable, setPlayerBet, showTable, initialDeal,
11 changeChipStack, setCardDeck, dealCards, resetTable, scoreHands,
12 dealCardToUser, handOver, setPlayerPasses) where -- note: export dealCardToUser only for ghci development
13
14 import Control.Lens
15
16 import Card
17 import Data.Bool
18 import Data.Maybe (fromMaybe)
19
20 data Table = Table { _numPlayers :: Int
21 , _chipStacks :: [Int] -- ^ number of chips, indexed by player index
22 , _dealtCards :: [[Card]] -- ^ dealt cards for user, dealer, and other players
23 , _currentPlayerBet :: Int
24 , _userPasses :: Bool
25 , _cardDeck :: [Card]
26 }
27 deriving (Show)
28
29 type Players = Int
30
31 createNewTable :: Players -> Table
32 createNewTable n =
33 Table n
34 [500 | _ <- [1 .. n]] -- give each player (incuding dealer) 10 chips
35 [[] | _ <- [0..n]] -- dealt cards for user and other players (we don't track dealer's chips)
36 20 -- currentPlayerBet
37 False
38 [] -- placeholder for random shuffled card deck
39
40 resetTable :: [Card] -> Table -> Int -> Table
41 resetTable cardDeck aTable numberOfPlayers =
42 Table numberOfPlayers
43 (_chipStacks aTable)
44 [[] | _ <- [0..numberOfPlayers]]
45 (_currentPlayerBet aTable)
46 False
47 cardDeck
48
49 -- Use lens extensions:
50
51 makeLenses ''Table
52
53 showDealtCards :: [[Card]] -> String
54 showDealtCards dc =
55 (show [map cardValue hand | hand <- dc])
56
57 setCardDeck :: [Card] -> Table -> Table
58 setCardDeck newDeck =
59 over cardDeck (\_ -> newDeck)
60
61 dealCards :: Table -> [Int] -> Table
62 dealCards aTable playerIndices =
63 last $ scanl dealCardToUser aTable playerIndices
64
65 -- | Initial deal: reset table with a new shuffled deck, then deal two rounds.
66 initialDeal :: [Card] -> Table -> Int -> Table
67 initialDeal cardDeck aTable numberOfPlayers =
68 dealCards
69 (dealCards (resetTable cardDeck aTable numberOfPlayers) [0 .. numberOfPlayers])
70 [0 .. numberOfPlayers]
71
72 showTable :: Table -> [Char]
73 showTable aTable =
74 "\nCurrent table data:\n" ++
75 " Chipstacks: " ++
76 "\n Player: " ++ (show (head (_chipStacks aTable))) ++
77 "\n Other players: " ++ (show (tail (_chipStacks aTable))) ++
78 "\n User cards: " ++ (show (head (_dealtCards aTable))) ++
79 "\n Dealer cards: " ++ (show ((_dealtCards aTable) !! 1)) ++
80 "\n Other player's cards: " ++ (show (tail (tail(_dealtCards aTable)))) ++
81 -- "\n Dealt cards: " ++ (show (_dealtCards aTable)) ++
82 "\n Dealt card values: " ++ (showDealtCards (_dealtCards aTable)) ++
83 "\n Current player bet: " ++
84 (show (_currentPlayerBet aTable)) ++
85 "\n Player pass: " ++
86 (show (_userPasses aTable)) ++ "\n"
87
88 clipScore :: Table -> Int -> Int
89 clipScore aTable playerIndex =
90 let s = score aTable playerIndex in
91 if s < 22 then s else 0
92
93 -- | Resolve bets for the hand: compare each player's score against the dealer.
94 -- Busts are treated as 0; updates chip stacks accordingly.
95 --
96 -- Indexing: chipStacks2 !! 0 = human, !! 1 = dealer, !! 2+ = AI players.
97 -- 'otherScores' starts at player index 2, so we zip with 'tail (tail chipStacks2)'
98 -- to skip the human's and dealer's chip entries.
99 scoreHands :: Table -> Table
100 scoreHands aTable =
101 let chipStacks2 = _chipStacks aTable
102 playerScore = clipScore aTable 0
103 dealerScore = clipScore aTable 1
104 otherScores = map (clipScore aTable) [2..]
105 newPlayerChipStack = if playerScore > dealerScore then
106 (head chipStacks2) + (_currentPlayerBet aTable)
107 else
108 if playerScore < dealerScore then
109 (head chipStacks2) - (_currentPlayerBet aTable)
110 else (head chipStacks2)
111 newOtherChipsStacks =
112 map (\(x,y) -> if x > dealerScore then
113 y + 20
114 else
115 if x < dealerScore then
116 y - 20
117 else y)
118 (zip otherScores (tail (tail chipStacks2)))
119 dealerChipStack = chipStacks2 !! 1
120 newChipStacks = newPlayerChipStack : dealerChipStack : newOtherChipsStacks
121 in
122 over chipStacks (\_ -> newChipStacks) aTable
123
124 setPlayerBet :: Int -> Table -> Table
125 setPlayerBet newBet =
126 over currentPlayerBet (\_ -> newBet)
127
128 setPlayerPasses :: Table -> Table
129 setPlayerPasses aTable =
130 let numPlayers = _numPlayers aTable
131 playerIndices = [1..numPlayers]
132 t1 = over userPasses (\_ -> True) aTable
133 t2 = dealCards t1 playerIndices
134 t3 = dealCards t2 playerIndices
135 t4 = dealCards t3 playerIndices
136 in
137 t4
138
139
140 changeChipStack :: Int -> Int -> Table -> Table
141 changeChipStack playerIndex newValue =
142 over chipStacks (\a -> a & element playerIndex .~ newValue)
143
144 score :: Table -> Int -> Int
145 score aTable playerIndex =
146 let scores = map cardValue ((_dealtCards aTable) !! playerIndex)
147 totalScore = sum scores in
148 totalScore
149
150 dealCardToUser' :: Table -> Int -> Table
151 dealCardToUser' aTable playerIndex =
152 let nextCard = head $ _cardDeck aTable
153 playerCards = nextCard : ((_dealtCards aTable) !! playerIndex)
154 newTable = over cardDeck (\cd -> tail cd) aTable in
155 over dealtCards (\a -> a & element playerIndex .~ playerCards) newTable
156
157 -- | Dealer/AI rule: user always draws; other players draw until score >= 17.
158 dealCardToUser :: Table -> Int -> Table
159 dealCardToUser aTable playerIndex
160 | playerIndex == 0 = dealCardToUser' aTable playerIndex -- user
161 | otherwise = if (score aTable playerIndex) < 17 then
162 dealCardToUser' aTable playerIndex
163 else aTable
164
165 handOver :: Table -> Bool
166 handOver aTable =
167 _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 -- Main.hs – entry point; collects player count then hands off to the TUI
2 module Main where
3
4 import Card -- pure code (card types + values)
5 import Table -- pure code (game state + rules)
6 import RandomizedList -- impure code (random shuffle)
7 import TUI -- Brick-based terminal UI
8 import Text.Read (readMaybe)
9
10 randomDeck :: IO [Card]
11 randomDeck = randomizedList orderedCardDeck
12
13 -- | Prompt for the number of other players, validating input.
14 getPlayerCount :: IO Int
15 getPlayerCount = do
16 putStrLn "Besides yourself, how many other players do you want at the table? (1-4)"
17 s <- getLine
18 case readMaybe s :: Maybe Int of
19 Just n | n >= 1 && n <= 4 -> return (n + 1) -- 0=user, 1=dealer, 2+= other players
20 _ -> do
21 putStrLn "Invalid input. Please enter a number between 1 and 4."
22 getPlayerCount
23
24 main :: IO ()
25 main = do
26 putStrLn "♠ ♥ Welcome to Blackjack! ♦ ♣"
27 n <- getPlayerCount
28 cardDeck <- randomDeck
29 let aTable = initialDeal cardDeck (createNewTable n) n
30 runTUI aTable n
This module combines the previously defined Card and Table modules with an impure RandomizedList module and a TUI module (Brick-based terminal UI) to implement a Blackjack card game.
Core Functions
randomDeck: Generates a randomized version of theorderedCardDeckusing therandomizedListfunction from theRandomizedListmodule.getPlayerCount: Prompts the user for the number of additional players (1-4), validating input withreadMaybeand recursing on invalid input. Returns the total player count (adding 1 for the dealer).-
main:- Prints a welcome banner.
- Calls
getPlayerCountto get the validated number of players. - Creates a new table with the specified number of players and an initial deal.
- Launches the Brick-based terminal UI via
runTUI.
Key Points
- The code demonstrates an interactive card game implementation using a terminal UI library (Brick).
- It combines pure modules (
Card,Table) with impure modules (RandomizedListfor randomization,TUIfor the terminal interface). - Input validation is handled using
readMaybefor safe parsing. - The
RandomizedListmodule provides a functionrandomizedListfor 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.