5
\$\begingroup\$

I created this Blackjack program as a first attempt at Ruby. Would appreciate a code review to see if I am doing things the Ruby way (is there a better way to get user input?), using OOP correctly and anything else you want to point out.

class Card

      def initialize(suit, face_value)
        @suit = suit
        @face_value = face_value
      end

      def get_value
        card_value = [@suit, @face_value]
      end
    end

    class Deck

      def initialize(number_of_decks = 1)
        @deck = create_deck(number_of_decks)
      end

      def draw(number_of_cards = 1)
        drawn_cards = []
        number_of_cards.times do
          drawn_cards << @deck.pop()
        end
        return drawn_cards
      end

      def size
        @deck.size
      end

      private

      def create_deck(number_of_decks)
        suite = [:Spades, :Clubs, :Hearts, :Diamonds]
        face_value = (2..10).to_a
        face_value << :Jack << :Queen << :King << :Ace

        deck = []

        number_of_decks.times do
          suite.each do |s|
            face_value.each do |v|
              card = Card.new(s, v)
              deck << card
            end
          end
        end

        deck.shuffle!
        return deck
      end
    end

    class Player
      attr_reader :score
      attr_reader :funds
      attr_reader :bet
      attr_reader :hand

      def initialize
        @hand = [];
        @score = 0;
        @bet = 0;
        @funds = 1000;
      end

      def reset
        @hand = [];
        @score = 0;
        @bet = 0;
      end

      def hand(cards)
        @hand += cards
        calculate_score
      end

      def place_bet
        amount = 0
        while true
          begin
            amount = Integer(gets)
            if amount <= @funds && amount > 0
              @bet += amount
              @funds -= amount
              break
            elsif amount > @funds
              puts "Sadly, you don't have enough funds to place this bet. Please enter a lower bet."
            else
              puts "You cannot place a zero or negative bet."
            end
          rescue
            puts "Please enter a valid whole amount."
          end
        end
      end

      def update_funds(amount)
        @funds += amount
      end

      def calculate_score
        @score = 0
        ace_in_hand = false

        @hand.each do |h|
         face_value = h.get_value[1]
         if face_value.is_a? Integer
           @score += face_value
         elsif face_value != :Ace
           @score += FACEVALUE
         else
            # Its an ace so could be 11 or 1?! However, only one ace could be counted as 11 (as 11 + 11 = 22 and would lose). Record if there is an ace in the hand and add one to the score.
           @score += 1
           ace_in_hand = true
         end
        end

        if ace_in_hand
          if @score + FACEVALUE <= BLACKJACK then @score += FACEVALUE end
        end
      end

      def print_hand
        hand_as_string = ""
        @hand.each do |card|
          card_value = card.get_value
          hand_as_string += "#{card_value[0]}, #{card_value[1]} | "
        end
        return hand_as_string
      end
    end

    def determine_winner(player, dealer)

      amount = 0

      if dealer.score > BLACKJACK && player.score > BLACKJACK
        puts "\n**No one wins!**\n"
      elsif player.score < dealer.score && dealer.score <= BLACKJACK || 
        player.score > BLACKJACK
        puts "\n***Dealer wins***\n"
      elsif dealer.score < player.score && player.score <= BLACKJACK ||
        dealer.score > BLACKJACK
        puts "\n****Player wins!****\n"
        # increase funds to player and check for blackjack
        if player.score == BLACKJACK && player.hand.size == 2
          amount = player.bet + player.bet * 1.5
        else
          amount = player.bet * 2
        end
      else
        puts "\nIt's a draw!\n"
        # return funds to player
        amount = player.bet
      end

      player.update_funds(amount)

    end

    # define blackjack winning score and value of face card
    BLACKJACK = 21;
    FACEVALUE = 10;
    DEALER_STANDS_AT = 17;

    puts "Welcome to blackjack!"
    puts "You currently have $1000 in funds. Blackjack pays 3:2"
    puts "How many decks should we use? (Integer between 1 and 10)"
    number_of_decks = -1
    while number_of_decks == -1
      begin
        number_of_decks = Integer(gets)
        if number_of_decks <= 0 || number_of_decks > 10
          number_of_decks = -1
          puts "The number of decks must be between 1 and 10."
        end
      rescue
        puts "Please enter a valid integer"
      end
    end

    keep_playing = true

    # Create the players and dealer
    player = Player.new
    dealer = Player.new

    while keep_playing

      # Create a new deck
      deck = Deck.new(number_of_decks)
      # Reset the players and dealers score and hand, and the players bet
      player.reset
      dealer.reset

      # Ask player to place bet
      puts "How much would you like to bet?"
      player.place_bet

      # Deal two cards to each player and one card to the dealer
      player.hand(deck.draw(2))
      dealer.hand(deck.draw)

      puts "\nYour current hand is: #{player.print_hand} \nYour score is: #{player.score}\n."

      puts "The dealer's hand is: #{dealer.print_hand}\nThe dealers score is: #{dealer.score}\n"

      # Deal the dealers second card (that is unknown to the player)
      dealer.hand(deck.draw)

      if dealer.score != BLACKJACK    
        # until player is not bust or stands
        until player.score > BLACKJACK do
          puts "\nDo you want to hit (h) or stand (s)?"
          players_move = String(gets)

          case players_move[0].downcase
          when "s"
            # stand
            break
          when "h"
            # hit
            puts "\nYou decided to hit!\n"
            player.hand(deck.draw)
            puts "\nYour hand is #{player.print_hand}\nYour score is: #{player.score}."
          else
            puts "Unknow input. Please try again.\n"
          end
        end

        puts "\nYour move has ended, resolving the dealers hand.\n"
        # Resolve the dealers hand (keep drawing cards until bust or >=17 reached)
        while dealer.score <= DEALER_STANDS_AT
          dealer.hand(deck.draw)
        end

        puts "\nThe dealer's hand is #{dealer.print_hand} \nThe dealers score is: #{dealer.score}"
      end

      # Determine the winner(s) and resolve any ties and settle all the bets
      determine_winner(player, dealer)
      puts "At the end of the game your funds stand at $#{player.funds}."

      # Ask if player wants to play again?
      while true
        puts "Do you want to play again? (Y/N)"
        play_again = String(gets)
        case play_again[0].downcase
        when "y" 
          break
        when "n"
          keep_playing = false
          break
        end
      end
    end
\$\endgroup\$
1
  • \$\begingroup\$ This is a good question but please fix your indentation by selecting the code and pressing Control-K \$\endgroup\$
    – Caridorc
    Commented Sep 17, 2016 at 21:41

2 Answers 2

1
\$\begingroup\$

create_deck

          def create_deck(number_of_decks)
            suite = [:Spades, :Clubs, :Hearts, :Diamonds]
            face_value = (2..10).to_a
            face_value << :Jack << :Queen << :King << :Ace

            deck = []

            number_of_decks.times do
              suite.each do |s|
                face_value.each do |v|
                  card = Card.new(s, v)
                  deck << card
                end
              end
            end

            deck.shuffle!
            return deck
          end
  1. The function really creates any number of decks, not just one.

  2. There is no need to modify an array many times, it is simpler to just create it.

  3. This looks like a textbook case of usage for the cartesian product

def create_deck
  suite = [:Spades, :Clubs, :Hearts, :Diamonds]
  face_value = (2..10).to_a
  face_value << :Jack << :Queen << :King << :Ace

  suite.product(face_value).map {|s, v| Card.new(s, v)}
end

def create_decks(n)
  (0..n).map(create_deck).flatten.shuffle
end
\$\endgroup\$
2
\$\begingroup\$

First suggestion:

attr_reader :score, :funds, :bet, :hand

Second, you don't need those semicolons. If you don't need something, in Ruby usually it's considered good style to avoid it.

  def reset
    @hand = []
    @score = 0
    @bet = 0
  end

Your Card class uses really awkward way to access it's members. You return an array holding the data - but your object already hold all this data. What's the point? Just make those readable, and pass the Card object around. While we at this class, it could also have methods to check if it's a face or a pip. This would make code using it neater, by hiding face_value.is_a? Integer where it belongs. Finally, adding value method to it, would really simplify score calculating code:

class Card
  attr_reader :suit, :face_value

  def initialize(suit, face)
    @suit, @face_value = suit, face_value
  end

  def pip?
    face_value.is_a? Integer
  end
  # def face? 
  # def ace?
  def value
    return 1 if ace?
    face? ? FACE_VALUE : face_value
  end
end

# ...

@score = @hand.map(&:value).inject(&:+)

if @hand.any?(&:ace?) && @score + FACEVALUE <= BLACKJACK
  @score += FACEVALUE
end

It iterates over @hand twice, but readability seems well worth it, and hands probably aren't too big arrays.

if @score + FACEVALUE <= BLACKJACK then @score += FACEVALUE end

Never use than. Either use ?:, or indent.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.