diff --git a/10_Blackjack/ruby/blackjack.rb b/10_Blackjack/ruby/blackjack.rb new file mode 100644 index 00000000..f2346f44 --- /dev/null +++ b/10_Blackjack/ruby/blackjack.rb @@ -0,0 +1,20 @@ +require_relative "./game.rb" + +def intro + puts "Welcome to Blackjack" +end + +def ask_for_players_count + puts "How many of you want to join the table?" + return gets.to_i +end + +begin + intro + players_count = ask_for_players_count + Game.new(players_count).start +rescue SystemExit, Interrupt + exit +rescue => exception + p exception +end diff --git a/10_Blackjack/ruby/game.rb b/10_Blackjack/ruby/game.rb new file mode 100644 index 00000000..b73fb9f0 --- /dev/null +++ b/10_Blackjack/ruby/game.rb @@ -0,0 +1,152 @@ +require_relative "./model/hand.rb" +require_relative "./model/player.rb" +require_relative "./model/card_kind.rb" +require_relative "./model/pack.rb" + +class Game + + ALLOWED_HAND_ACTIONS = { + "hit" => ["H", "S"], + "split" => ["H", "S", "D"], + "normal" => ["H", "S", "/", "D"] + } + + def initialize(players_count) + @pack = Model::Pack.new + @dealer_balance = 0 + @dealer_hand = nil + @players = 1.upto(players_count).map { |id| Model::Player.new(id) } + end + + def start + loop do + collect_bets_and_deal + play_players + check_for_insurance_bets + play_dealer + settle + end + end + + private + + def collect_bets_and_deal + puts "BETS" + + @players.each_entry do |player| + print "# #{player.id} ? " + bet = gets.to_i + player.deal_initial_hand Model::Hand.new(bet, [@pack.draw, @pack.draw]) + end + + @dealer_hand = Model::Hand.new(0, [@pack.draw, @pack.draw]) + print_players_and_dealer_hands + end + + def play_players + @players.each_entry do |player| + play_hand player, player.hand + end + end + + def check_for_insurance_bets + return if @dealer_hand.cards[0].label != "A" + + print "ANY INSURANCE? " + return if gets.strip != "Y" + + @players.each_entry do |player| + print "PLAYER #{player.id} INSURANCE BET? " + player.bet_insurance(gets.to_i) + end + end + + def play_dealer + puts "DEALER HAS A \t#{@dealer_hand.cards[1].label} CONCEALED FOR A TOTAL OF #{@dealer_hand.total}" + + while @dealer_hand.total(is_dealer: true) < 17 + card = @pack.draw + @dealer_hand.hit card + + puts "DRAWS #{card.label} \t---TOTAL = #{@dealer_hand.total}" + end + + if !@dealer_hand.is_busted? + @dealer_hand.stand + end + end + + def settle + @players.each_entry do |player| + player_balance_update = player.update_balance @dealer_hand + @dealer_balance -= player_balance_update + + puts "PLAYER #{player.id} #{player_balance_update < 0 ? "LOSES" : "WINS"} \t#{player_balance_update} \tTOTAL=#{player.balance}" + end + + puts "DEALER'S TOTAL = #{@dealer_balance}" + end + + + def print_players_and_dealer_hands + puts "PLAYER\t#{@players.map(&:id).join("\t")}\tDEALER" + puts " \t#{@players.map {|p| p.hand.cards[0].label}.join("\t")}\t#{@dealer_hand.cards[0].label}" + puts " \t#{@players.map {|p| p.hand.cards[1].label}.join("\t")}" + end + + def play_hand player, hand + allowed_actions = ALLOWED_HAND_ACTIONS[(hand.is_split_hand || !hand.can_split?) ? "split" : "normal"] + name = "PLAYER #{player.id}" + if hand.is_split_hand + name += " - HAND #{hand === player.hand ? 1 : 2}" + end + + did_hit = false + + while hand.is_playing? + print "#{name}? " + + action = gets.strip + + if !allowed_actions.include?(action) + puts "Possible actions: #{allowed_actions.join(", ")}" + next + end + + if action === "/" + player.split + + play_hand player, player.hand + play_hand player, player.split_hand + + return + end + + if action === "S" + hand.stand + end + + if action === "D" + card = @pack.draw + hand.double_down card + + puts "RECEIVED #{card.label}" + end + + if action === "H" + did_hit = true + allowed_actions = ALLOWED_HAND_ACTIONS["hit"] + card = @pack.draw + hand.hit card + + puts "RECEIVED #{card.label}" + end + end + + puts "TOTAL IS #{hand.total}" + + if hand.is_busted? + puts "... BUSTED" + end + end +end diff --git a/10_Blackjack/ruby/model/card_kind.rb b/10_Blackjack/ruby/model/card_kind.rb new file mode 100644 index 00000000..0be340ce --- /dev/null +++ b/10_Blackjack/ruby/model/card_kind.rb @@ -0,0 +1,41 @@ +module Model +class CardKind + def initialize(label, value) + @label = label + @value = value + end + + private_class_method :new + + TWO = self.new("2", 2) + THREE = self.new("3", 3) + FOUR = self.new("4", 4) + FIVE = self.new("5", 5) + SIX = self.new("6", 6) + SEVEN = self.new("7", 7) + EIGHT = self.new("8", 8) + NINE = self.new("9", 9) + TEN = self.new("10", 10) + JACK = self.new("J", 10) + QUEEN = self.new("Q", 10) + KING = self.new("K", 10) + ACE = self.new("A", 11) + + KINDS_SET = [ + TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, + JACK, QUEEN, KING, ACE + ] + + def same_value?(other_card) + value == other_card.value + end + + def +(other) + throw "other doesn't respond to +" unless other.responds_to? :+ + + other.+(@value) + end + + attr_reader :label, :value +end +end diff --git a/10_Blackjack/ruby/model/hand.rb b/10_Blackjack/ruby/model/hand.rb new file mode 100644 index 00000000..e52052cf --- /dev/null +++ b/10_Blackjack/ruby/model/hand.rb @@ -0,0 +1,97 @@ +require_relative "./card_kind.rb" + +module Model +class Hand + HAND_STATE_PLAYING = :hand_playing + HAND_STATE_BUSTED = :hand_busted + HAND_STATE_STAND = :hand_stand + HAND_STATE_DOUBLED_DOWN = :hand_doubled_down + + def initialize(bet, cards, is_split_hand: false) + @state = HAND_STATE_PLAYING + @bet = bet + @cards = cards + @total = nil + @is_split_hand = is_split_hand + end + + attr_reader :bet, :cards, :is_split_hand + + def is_playing? + @state == HAND_STATE_PLAYING + end + + def is_busted? + @state == HAND_STATE_BUSTED + end + + def is_standing? + @state == HAND_STATE_STAND + end + + def is_blackjack? + total == 21 && @cards.length == 2 + end + + def total(is_dealer: false) + return @total unless @total.nil? + + @total = @cards.reduce(0) {|sum, card| sum + card.value} + + if @total > 21 + aces_count = @cards.count {|c| c == CardKind::ACE} + while ((!is_dealer && @total > 21) || (is_dealer && @total < 16)) && aces_count > 0 do + @total -= 10 + aces_count -= 1 + end + end + + @total + end + + ## Hand actions + + def can_split? + not @is_split_hand and @cards.length == 2 && @cards[0].same_value?(cards[1]) + end + + def split + throw "can't split" unless can_split? + [ + Hand.new(@bet, @cards[0...1], is_split_hand: true), + Hand.new(@bet, @cards[1..1], is_split_hand: true) + ] + end + + def hit(card) + throw "can't hit" unless is_playing? + + @cards.push(card) + @total = nil + + check_busted + end + + def double_down(card) + throw "can't double down" unless is_playing? + + @bet *= 2 + hit card + + @state = HAND_STATE_DOUBLED_DOWN + end + + def stand + throw "can't stand" unless is_playing? + + @state = HAND_STATE_STAND + end + + + private + + def check_busted + @state = HAND_STATE_BUSTED if total > 21 + end +end +end diff --git a/10_Blackjack/ruby/model/pack.rb b/10_Blackjack/ruby/model/pack.rb new file mode 100644 index 00000000..20c0f227 --- /dev/null +++ b/10_Blackjack/ruby/model/pack.rb @@ -0,0 +1,28 @@ +require_relative "./card_kind.rb" + +module Model +class Pack + def initialize + @cards = [] + reshuffle + end + + def reshuffle_if_necessary + return if @cards.count > 2 + reshuffle + end + + def draw + reshuffle_if_necessary + @cards.pop + end + + private + + def reshuffle + puts "RESHUFFLING" + @cards = 4.times.map {|_| CardKind::KINDS_SET}.flatten + @cards.shuffle! + end +end +end diff --git a/10_Blackjack/ruby/model/player.rb b/10_Blackjack/ruby/model/player.rb new file mode 100644 index 00000000..134cf2dc --- /dev/null +++ b/10_Blackjack/ruby/model/player.rb @@ -0,0 +1,89 @@ +require_relative "./hand.rb" + +module Model +class Player + def initialize(id) + @id = id + @balance = 0 + @original_bet = 0 + @insurance = 0 + + @hand = nil + @split_hand = nil + end + + attr_reader :id, :balance, :hand, :split_hand, :insurance + + ## Begining of hand dealing actions + def deal_initial_hand(hand) + @hand = hand + @split_hand = nil + @max_insurance = @hand.bet / 2 + @insurance = 0 + end + + def has_split_hand? + !@split_hand.nil? + end + + def can_split? + not has_split_hand? and @hand.can_split? + end + + def split + throw "can't split" unless can_split? + + @hand, @split_hand = @hand.split + end + + def bet_insurance(bet) + if bet < 0 + bet = 0 + puts "NEGATIVE BET -- using 0 insurance bet" + end + + if bet > @max_insurance + bet = @max_insurance + puts "TOO HIGH -- using max insurance bet of #{bet}" + end + + @insurance = bet + end + + ## End of hand dealing actions + + def update_balance(dealer_hand) + balance_update = 0 + + balance_update += get_balance_update(@hand, dealer_hand) + if has_split_hand? then + balance_update += get_balance_update(@split_hand, dealer_hand) + end + + if dealer_hand.is_blackjack? + balance_update += 2 * @insurance + else + balance_update -= @insurance + end + + @balance += balance_update + + balance_update + end + + + private + + def get_balance_update(hand, dealer_hand) + if hand.is_busted? + return -hand.bet + elsif dealer_hand.is_busted? + return hand.bet + elsif dealer_hand.total == hand.total + return 0 + else + return (dealer_hand.total < hand.total ? 1 : -1) * hand.bet + end + end +end +end