mirror of
https://github.com/coding-horror/basic-computer-games.git
synced 2026-02-04 11:07:59 -08:00
add initial implementation
This commit is contained in:
24
10_Blackjack/ruby/blackjack.rb
Normal file
24
10_Blackjack/ruby/blackjack.rb
Normal file
@@ -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
|
||||
136
10_Blackjack/ruby/game.rb
Normal file
136
10_Blackjack/ruby/game.rb
Normal file
@@ -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
|
||||
41
10_Blackjack/ruby/model/card_kind.rb
Normal file
41
10_Blackjack/ruby/model/card_kind.rb
Normal file
@@ -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
|
||||
93
10_Blackjack/ruby/model/hand.rb
Normal file
93
10_Blackjack/ruby/model/hand.rb
Normal file
@@ -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
|
||||
27
10_Blackjack/ruby/model/pack.rb
Normal file
27
10_Blackjack/ruby/model/pack.rb
Normal file
@@ -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
|
||||
68
10_Blackjack/ruby/model/player.rb
Normal file
68
10_Blackjack/ruby/model/player.rb
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user