Course:CPSC312-2023/Blackjack

From UBC Wiki

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>