Course:CPSC312-2023/Blackjack
Authors: Ryan Mehri, Steven Slater
What is the problem?
Blackjack is one of the most widely played card games in the world, so we want to see if we can implement it in Haskell along with some extra features! The game uses a number of standard decks of 52 cards depending on the number of players and each round starts by players placing bets. The goal of each round is to win by having cards that have a total higher than the dealer but not exceeding 21, which is a bust. Within a round, players take turns and can:
- Hit: take a card
- Stand: end their turn
- Double down: double their wager, take a card, and finish
- Split: make two hands from cards with the same value in the starting hand
- Surrender: give up a half-bet and retire from the game
Once the players finish, the dealer draws cards until the hand is a total of 17 or higher and the dealer never doubles, splits, or surrenders. If the dealer busts then all remaining players win.
For counting card values:
- Number cards have their value
- Face cards have value 10
- Aces count as either 1 or 11 according to the player
A total of 21 on the first two cards is called a blackjack and the player wins immediately unless the dealer also has one, which would result in a tie with bets returned without adjustment.
What is the something extra?
In our BlackJack game we added the functionality of betting that emulates the real game. The user is asked at the beginning of the game how much money they wish to deposit into their "Money Pot". After this they are then allowed to bet any amount within that pot.
This functionality caused us to change the way we dealt with the game state, as the money pool was a constant value that could increase or decrease across different games however we had to reset the player and dealer hands every game.
Adding this in allows for the game to have a sense of risk and this increases the enjoyment for the user playing our game.
What did we learn from doing this?
We have learnt many new ideas and topics under the functional programming umbrella from carrying out this project, some of the main takeaways are:
- Better understanding of Game design in Haskell, learning how to build a User Interface and how to break down your project into smaller code-able chunks. Building the project from the bottom up gave us a better understanding of what is required to build a game in Haskell, such as the states and different player types. In our game we implemented a "Money Pool" that the player bets from, as a result of this we had to change the way we updated our state after every round of BlackJack, this also added to our understanding of the state space of the game.
- Controlling user access, in our game of BlackJack it is imperative that the user does not have access or is able to see the deck of cards in that current state space, as a result of this we essentially had to control our state so only the player only has access to both hands of cards (theirs and the dealers) and the current bet. This is essentially a form of encapsulation in a functional programming language which was something that was new to us.
- Reducing side effects; as learnt in class if a Haskell program operates with a lot of I/O we can introduce side effects and reduce the efficiency of our program, so we learnt how to limit the amount of I/O in the program and only use it when necessary, as a result of this we had to use more functions which overall contributed to a deeper understanding of Haskell.
- In conclusion, we think that functional programming is really well suited to the task of creating games! It was really quite easy to test what would happen in certain states due to having no side effects and the standard library had many abstractions that fit our needs, leading to our code being fairly concise as seen below it is only around 200 lines. One such example is how non-determinism is captured by the List monad as we saw here: https://stackoverflow.com/questions/27265920/what-is-non-determinism-in-haskell, meaning that we could handle the multiple values that a hand could have due to aces fairly easily.
Code
To run the program, load it into ghci
and run the play
function to start. There is an example trace at the bottom of the wiki.
module Blackjack where import Control.Monad (forM_, when) import Data.Char (toUpper) import Data.List (nub) import Data.List.NonEmpty (nonEmpty) import Data.Maybe (isNothing) import GHC.Data.Maybe (fromJust) import System.Random (RandomGen, getStdGen, newStdGen, randomRs) import Text.Read (readMaybe) {- Data Definitions -} data State = State { playerHand :: Hand, dealerHand :: Hand, deck :: Deck, playerBalance :: Balance, playerBet :: Bet } deriving (Eq) instance Show State where show (State {playerHand, dealerHand, playerBet}) = showHand "Player" playerHand ++ "\n" ++ showHand "Dealer" dealerHand ++ "\n" ++ "Player Bet: $" ++ show playerBet where showHand prefix hand = prefix ++ " hand is " ++ show hand ++ " and " ++ valueText hand valueText hand = case handValue hand of Nothing -> "is a bust!" Just x -> "has value " ++ show x -- | The cards of interest for the game. -- We do not consider suit since only value is used in blackjack. data Card = Ace | Number Int | Jack | Queen | King deriving (Eq) instance Show Card where show Ace = "A" show (Number x) = show x show Jack = "J" show Queen = "Q" show King = "K" -- | The amount of money the player has in total type Balance = Double -- | The amount of money betted in one round type Bet = Double type Hand = [Card] type Deck = [Card] data Result = EndOfGame {playerWins :: Bool, state :: State} | ContinueGame State deriving (Eq, Show) type Game = Action -> State -> Result type Player = State -> Action -- | The possible actions a player can take. data Action = Hit | Stand deriving (Eq, Show) {- Game Functions -} -- | Definition of the blackjack game. blackjackGame :: Game blackjackGame Hit (State {playerHand, dealerHand, deck = (draw : remainingDeck), playerBet, playerBalance}) = if isBust newPlayerHand then EndOfGame {playerWins = False, state = newState} else ContinueGame newState where newPlayerHand = draw : playerHand newState = State {playerHand = newPlayerHand, dealerHand, deck = remainingDeck, playerBet, playerBalance} blackjackGame Stand state = resolveDealer state -- | Produces True if the given hand is a bust. isBust :: Hand -> Bool isBust = isNothing . handValue -- | The maximum possible value a hand can be, accounting for Aces. -- None if hand is a bust. handValue :: Hand -> Maybe Int handValue hand = fmap maximum (nonEmpty validValues) where validValues = nub (filter (<= 21) possibleValues) possibleValues = map sum (mapM cardValues hand) -- | The possible values a card can be. cardValues :: Card -> [Int] cardValues Ace = [1, 11] cardValues Jack = [10] cardValues Queen = [10] cardValues King = [10] cardValues (Number x) = [x] -- | Resolves the dealers state and returns the result of the game. resolveDealer :: State -> Result resolveDealer state = EndOfGame {playerWins, state = newState} where newState@State {playerHand, dealerHand} = drawForDealer state playerWins = isBust dealerHand || fromJust (handValue dealerHand) < fromJust (handValue playerHand) -- | Draw until the dealer has a value of at least 17. drawForDealer :: State -> State drawForDealer state@(State {playerHand, dealerHand, deck = draw : remainingDeck, playerBet, playerBalance}) = case handValue dealerHand of Nothing -> state Just x -> if x >= 17 then state else drawForDealer (State {playerHand, dealerHand = draw : dealerHand, deck = remainingDeck, playerBet, playerBalance}) -- | Main function to play the game, interacting with the user. play :: IO () play = do putStrLn "Welcome to Blackjack! How much would you like to put in your balance?" balance <- getAmount rg <- newStdGen let startingState = mkStartingState balance (mkDeck rg) playRound startingState -- | Plays a single round, taking a bet and updating the balance after the game. playRound :: State -> IO () playRound initState@(State {playerHand, dealerHand, deck, playerBalance, playerBet}) = do putStrLn "How much would you like to bet this round?" bet <- getAmount if bet > playerBalance then do putStrLn "Bet exceeds your balance!" playRound initState else do let state = State {playerHand, dealerHand, deck, playerBalance, playerBet = bet} nextState <- gameLoop state forM_ nextState playRound -- | The game loop for a single blackjack game, repeatedly asking for input and changing the game state. -- Return nothing if the game is over. gameLoop :: State -> IO (Maybe State) gameLoop state = do print state action <- getAction when (action == Stand) (putStrLn "Resolving dealer hand...") case blackjackGame action state of EndOfGame {playerWins, state = endState@State {playerHand, dealerHand, deck, playerBalance, playerBet}} -> do print endState let newBalance = if playerWins then playerBalance + playerBet else playerBalance - playerBet if playerWins then putStrLn ("Player wins!" ++ " You won: $" ++ show (2 * playerBet)) else putStrLn "Dealer wins!" putStrLn ("New Balance: $" ++ show newBalance) when (newBalance <= 0) (putStrLn "You are out of money :( Thanks for playing!") return ( if newBalance > 0 then Just (mkStartingState newBalance deck) else Nothing ) ContinueGame nextState -> gameLoop nextState getAmount :: IO Double getAmount = do line <- getLine case readMaybe line :: Maybe Double of Nothing -> do putStrLn "Invalid amount, please enter a valid double." getAmount Just x -> if x > 0 then return x else do putStrLn "Must be a positive amount." getAmount getAction :: IO Action getAction = do putStrLn "[H]it or [S]tand?" line <- getLine let upperLine = map toUpper line case upperLine of "H" -> return Hit "S" -> return Stand _ -> getAction {- Helpers -} -- | Make a random starting state by creating a deck and dealing cards to the players. mkStartingState :: Balance -> Deck -> State mkStartingState playerBalance deck = State {playerHand, dealerHand, deck = newDeck, playerBet = 0, playerBalance} where playerHand = [playerCard1, playerCard2] dealerHand = [dealerCard1, dealerCard2] (playerCard1 : playerCard2 : dealerCard1 : dealerCard2 : newDeck) = deck -- | Make a deck by generating an infinite list of cards. -- Note this is not how an actual deck works but is a simple approximation. mkDeck :: RandomGen g => g -> Deck mkDeck rg = map mkCard (randomRs (1, 13) rg) -- | Construct a card from an integer value. mkCard :: Int -> Card mkCard 1 = Ace mkCard 11 = Jack mkCard 12 = Queen mkCard 13 = King mkCard x = Number x
Example Trace
❯ ghci blackjack.hs Loaded package environment from /Users/rmehri01/.ghc/aarch64-darwin-9.2.5/environments/default GHCi, version 9.2.5: https://www.haskell.org/ghc/ :? for help [1 of 1] Compiling Blackjack ( blackjack.hs, interpreted ) Ok, one module loaded. ghci> play Welcome to Blackjack! How much would you like to put in your balance? 800 How much would you like to bet this round? 400 Player hand is [2,Q] and has value 12 Dealer hand is [3,Q] and has value 13 Player Bet: $400.0 [H]it or [S]tand? h Player hand is [8,2,Q] and has value 20 Dealer hand is [3,Q] and has value 13 Player Bet: $400.0 [H]it or [S]tand? s Resolving dealer hand... Player hand is [8,2,Q] and has value 20 Dealer hand is [Q,3,Q] and is a bust! Player Bet: $400.0 Player wins! You won: $800.0 New Balance: $1200.0 How much would you like to bet this round? 1200 Player hand is [3,K] and has value 13 Dealer hand is [A,A] and has value 12 Player Bet: $1200.0 [H]it or [S]tand? h Player hand is [10,3,K] and is a bust! Dealer hand is [A,A] and has value 12 Player Bet: $1200.0 Dealer wins! New Balance: $0.0 You are out of money :( Thanks for playing! ghci>