From b38cf0018ad011a1bef1797935585bf4812df277 Mon Sep 17 00:00:00 2001 From: Alaa Sarhan Date: Tue, 29 Mar 2022 00:31:45 +0200 Subject: [PATCH 1/3] add initial implementation --- 10_Blackjack/ruby/blackjack.rb | 24 +++++ 10_Blackjack/ruby/game.rb | 136 +++++++++++++++++++++++++++ 10_Blackjack/ruby/model/card_kind.rb | 41 ++++++++ 10_Blackjack/ruby/model/hand.rb | 93 ++++++++++++++++++ 10_Blackjack/ruby/model/pack.rb | 27 ++++++ 10_Blackjack/ruby/model/player.rb | 68 ++++++++++++++ 6 files changed, 389 insertions(+) create mode 100644 10_Blackjack/ruby/blackjack.rb create mode 100644 10_Blackjack/ruby/game.rb create mode 100644 10_Blackjack/ruby/model/card_kind.rb create mode 100644 10_Blackjack/ruby/model/hand.rb create mode 100644 10_Blackjack/ruby/model/pack.rb create mode 100644 10_Blackjack/ruby/model/player.rb diff --git a/10_Blackjack/ruby/blackjack.rb b/10_Blackjack/ruby/blackjack.rb new file mode 100644 index 00000000..a6d08271 --- /dev/null +++ b/10_Blackjack/ruby/blackjack.rb @@ -0,0 +1,24 @@ +require_relative "./game.rb" + +# TODOS +# 1. check if we should implement insurances +# 2. test splitting + +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..d7bfdea3 --- /dev/null +++ b/10_Blackjack/ruby/game.rb @@ -0,0 +1,136 @@ +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 + 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 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" + # TODO: Check for split hands + 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}" + 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 "#{name} - Hand 1", player.hand + play_hand "#{name} - Hand 2", 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..101900c7 --- /dev/null +++ b/10_Blackjack/ruby/model/hand.rb @@ -0,0 +1,93 @@ +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 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..3fc64df7 --- /dev/null +++ b/10_Blackjack/ruby/model/pack.rb @@ -0,0 +1,27 @@ +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 + @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..c71a327d --- /dev/null +++ b/10_Blackjack/ruby/model/player.rb @@ -0,0 +1,68 @@ +require_relative "./hand.rb" + +module Model +class Player + def initialize(id) + @id = id + @balance = 0 + + @hand = nil + @split_hand = nil + end + + attr_reader :id, :balance, :hand, :split_hand + + ## Begining of hand dealing actions + def deal_initial_hand(hand) + @hand = hand + @split_hand = nil + 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(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 + + @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 From f3399cd34bc5348f5fb46a03608a5ffff0805540 Mon Sep 17 00:00:00 2001 From: Alaa Sarhan Date: Tue, 29 Mar 2022 20:54:42 +0200 Subject: [PATCH 2/3] test and fix splitting --- 10_Blackjack/ruby/blackjack.rb | 4 ---- 10_Blackjack/ruby/game.rb | 9 ++++++--- 10_Blackjack/ruby/model/hand.rb | 2 +- 10_Blackjack/ruby/model/pack.rb | 1 + 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/10_Blackjack/ruby/blackjack.rb b/10_Blackjack/ruby/blackjack.rb index a6d08271..f2346f44 100644 --- a/10_Blackjack/ruby/blackjack.rb +++ b/10_Blackjack/ruby/blackjack.rb @@ -1,9 +1,5 @@ require_relative "./game.rb" -# TODOS -# 1. check if we should implement insurances -# 2. test splitting - def intro puts "Welcome to Blackjack" end diff --git a/10_Blackjack/ruby/game.rb b/10_Blackjack/ruby/game.rb index d7bfdea3..cc595d85 100644 --- a/10_Blackjack/ruby/game.rb +++ b/10_Blackjack/ruby/game.rb @@ -77,7 +77,6 @@ class Game def print_players_and_dealer_hands puts "PLAYER\t#{@players.map(&:id).join("\t")}\tDEALER" - # TODO: Check for split hands 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 @@ -85,6 +84,10 @@ class Game 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? @@ -100,8 +103,8 @@ class Game if action === "/" player.split - play_hand "#{name} - Hand 1", player.hand - play_hand "#{name} - Hand 2", player.split_hand + play_hand player, player.hand + play_hand player, player.split_hand return end diff --git a/10_Blackjack/ruby/model/hand.rb b/10_Blackjack/ruby/model/hand.rb index 101900c7..c241e1bb 100644 --- a/10_Blackjack/ruby/model/hand.rb +++ b/10_Blackjack/ruby/model/hand.rb @@ -54,7 +54,7 @@ class Hand def split throw "can't split" unless can_split? [ - Hand.new(@bet, @cards[0..1], is_split_hand: true), + Hand.new(@bet, @cards[0...1], is_split_hand: true), Hand.new(@bet, @cards[1..1], is_split_hand: true) ] end diff --git a/10_Blackjack/ruby/model/pack.rb b/10_Blackjack/ruby/model/pack.rb index 3fc64df7..20c0f227 100644 --- a/10_Blackjack/ruby/model/pack.rb +++ b/10_Blackjack/ruby/model/pack.rb @@ -13,6 +13,7 @@ class Pack end def draw + reshuffle_if_necessary @cards.pop end From 3130a3d74dcbd475b509392c55c5c252e3e686ac Mon Sep 17 00:00:00 2001 From: Alaa Sarhan Date: Tue, 29 Mar 2022 21:29:00 +0200 Subject: [PATCH 3/3] implement insurance bets --- 10_Blackjack/ruby/game.rb | 13 +++++++++++++ 10_Blackjack/ruby/model/hand.rb | 4 ++++ 10_Blackjack/ruby/model/player.rb | 25 +++++++++++++++++++++++-- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/10_Blackjack/ruby/game.rb b/10_Blackjack/ruby/game.rb index cc595d85..b73fb9f0 100644 --- a/10_Blackjack/ruby/game.rb +++ b/10_Blackjack/ruby/game.rb @@ -22,6 +22,7 @@ class Game loop do collect_bets_and_deal play_players + check_for_insurance_bets play_dealer settle end @@ -48,6 +49,18 @@ class Game 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}" diff --git a/10_Blackjack/ruby/model/hand.rb b/10_Blackjack/ruby/model/hand.rb index c241e1bb..e52052cf 100644 --- a/10_Blackjack/ruby/model/hand.rb +++ b/10_Blackjack/ruby/model/hand.rb @@ -29,6 +29,10 @@ class Hand @state == HAND_STATE_STAND end + def is_blackjack? + total == 21 && @cards.length == 2 + end + def total(is_dealer: false) return @total unless @total.nil? diff --git a/10_Blackjack/ruby/model/player.rb b/10_Blackjack/ruby/model/player.rb index c71a327d..134cf2dc 100644 --- a/10_Blackjack/ruby/model/player.rb +++ b/10_Blackjack/ruby/model/player.rb @@ -5,17 +5,21 @@ 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 + 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? @@ -32,7 +36,18 @@ class Player @hand, @split_hand = @hand.split end - def bet_insurance(insurance_bet) + 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 @@ -45,6 +60,12 @@ class Player 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