require 'cards'

module Blackjack
	include Cards

	TwentyOne = 21
	
	DealerHitLimit = 17

	Value::Ace.to_i = 11
	Value::Jack.to_i = 10
	Value::Queen.to_i = 10
	Value::King.to_i = 10

	class Cards::Card
		def to_i
			value.to_i
		end
	end

	def self.hand_value(cards)
		val = cards.inject(0) {|sum, card| sum += card.to_i }
		aces = cards.find_all {|card| card.value == Value::Ace }
		while val > TwentyOne && aces.length > 0
			val -= 10
			aces.pop
		end
		[val, (aces.length unless aces.length == 0)] unless val > TwentyOne
	end

	class ::Array
		include Cards::Deck
		def value
			val = Blackjack::hand_value(self)
			val[0] unless val.nil?
		end
		def soft?
			val = Blackjack::hand_value(self)
			val[1] unless val.nil?
		end
		def busted?
			val = Blackjack::hand_value(self)
			val.nil? || val[0] == 0
		end
		def natural?
			length == 2 && Blackjack::hand_value(self) == 21
		end
		alias :old_to_s :to_s
		def to_s
			if length > 0 && Cards::Card === self[0]
				val = Blackjack::hand_value(self)
				self.join(' ') +
					' (' +
					(val ? val[0].to_s + (val[1] ? ', soft' : '') : 'busted') +
					')'
			else
				old_to_s
			end
		end
	end

	Player = Struct.new(:name, :bankroll, :hands)

	Hand = Struct.new(:bet, :cards)
	class Hand
		def method_missing(*args)
			cards.send(*args)
		end
	end

	class Action
		attr_reader :shortcut_key
		@@instances = []
		def self.instances
			@@instances
		end
		def self.instances_allowed(player, hand)
			@@instances.find_all {|action| action.allowed?(player, hand) }
		end
		def self.[](index)
			i = index.downcase
			@@instances.find {|action| action.to_s.downcase == i || action.shortcut_key.downcase == i }
		end
		def initialize(name, allow_proc, apply_proc, may_continue = true)
			@name = name
			shortcut_keys = name + ('a'..'z').to_a.join + ('0'..'9').to_a.join
			i = 0
			while @@instances.find {|action| action.shortcut_key == shortcut_keys[i..i] }
				i += 1
			end
			@shortcut_key = shortcut_keys[i..i]
			@allow_proc = allow_proc
			@apply_proc = apply_proc
			@may_continue = may_continue
			@@instances << self
		end
		def to_s
			@name.split(//).map{|c| c == @shortcut_key ? c.upcase : c.downcase }.join
		end
		def allowed?(player, hand)
			@allow_proc.call(player, hand)
		end
		def apply(player, hand, deck)
			@apply_proc.call(player, hand, deck)
		end
		def may_continue?
			@may_continue
		end
		Stand = Action.new(
			'stand',
			Proc.new { true },
			Proc.new {},
			false)
		Hit = Action.new(
			'hit',
			Proc.new {|player, hand| (v = hand.value) && v < TwentyOne },
			Proc.new {|player, hand, deck|
				hand << deck.draw
			},
			true)
		Double = Action.new(
			'double',
			Proc.new {|player, hand| hand.cards.length == 2 &&
			                         player.bankroll >= hand.bet &&
			                         hand.value < TwentyOne },
			Proc.new {|player, hand, deck|
				player.bankroll -= hand.bet
				hand.bet *= 2
				hand << deck.draw
			},
			false)
		Split = Action.new(
			'split',
			Proc.new {|player, hand| hand.cards.length == 2 &&
			                         hand.cards[0].value == hand.cards[1].value &&
			                         player.bankroll >= hand.bet },
			Proc.new {|player, hand, deck|
				player.bankroll -= hand.bet
				player.hands << Hand.new(hand.bet, [hand.pop, deck.draw])
				hand << deck.draw
			},
			true)
	end

	#
	# Command-line Blackjack
	#
	def self.cli
		table_class = Struct.new(:min_bet, :max_bet, :dealer, :players, :deck, :discard)
		table = table_class.new
		table.min_bet = 5
		table.max_bet = 20
		table.dealer = dealer = Player.new('The Dealer')
		table.players = players = []
		table.deck = deck = Deck.new.shuffle!
		table.discard = discard = []
		loan_shark = []
		
		class << loan_shark
			attr_accessor :max_loan, :interest_rate
		end
		loan_shark.max_loan = 500
		loan_shark.interest_rate = 0.01
		
		class << deck
			attr_accessor :discard
			def draw
				if length == 0
					concat(discard)
					@discard.delete_if {true}
					shuffle!
				end
				shift
			end
		end
		deck.discard = discard
		
		puts "/A blackjack table in a dim room. A scratchy voice speaks./"
		#
		# Main play loop
		#
		begin
			#
			# Add new players
			#
			begin
				print "OK, who's just arrived? "
				newb = gets.chomp
				if !newb.empty?
					puts "Hi, #{newb}, and welcome to Blackjack."
					print "I'll be your loan shark for the duration. Any cash out? $"
					cash_out = gets.to_i
					2.times do
						if cash_out <= 0
							print "You need money if you want to play. How much? $"
							cash_out = gets.to_i
						elsif cash_out > loan_shark.max_loan
							print "That's a bit rich for me. You can have up to $#{loan_shark.max_loan}. How much? "
							cash_out = gets.to_i
							cash_out = loan_shark.max_loan if cash_out == 0
						end
					end
					if cash_out > 0 && cash_out <= loan_shark.max_loan
						newb = Player.new(newb, cash_out)
						loan_shark << cash_out
						puts "You can have a seat at the table now, #{newb.name}."
						players << newb
					end
				end
			end while Player === newb
			
			#
			# Bets in!
			#
			puts "Let's get started then, shall we?"
			puts "Limits for this table are $#{table.min_bet}-$#{table.max_bet}. Bets in!"
			players.each do |player|
				print "#{player.name}, you have $#{player.bankroll}. Bet. $"
				bet = gets.to_i
				if !bet.between?(table.min_bet, table.max_bet) && bet != 0 && bet <= player.bankroll
					puts "Bets on this table must be between $#{table.min_bet} and $#{table.max_bet}."
					print "#{player.name}? $"
					bet = gets.to_i
				end
				if bet.between?(table.min_bet, table.max_bet)
					player.hands = [Hand.new(bet)]
					player.bankroll -= bet
				end
			end
			
			#
			# Deal
			#
			players.each do |player|
				player.hands[0].cards = [deck.draw, deck.draw] if player.hands
			end
			dealer.hands = [Hand.new(nil, [deck.draw, deck.draw])]
			puts "The dealer's showing #{dealer.hands[0].cards[0]}."
			
			#
			# Players play
			#
			players.each do |player|
				next if !player.hands
				hand_index = 0
				while hand_index < player.hands.length
					hand = player.hands[hand_index]
					player_may_continue = true
					while player_may_continue && (options = Action.instances_allowed(player, hand)).length > 1
						
						print "#{player.name}, you have: #{hand.cards}. "
						option = nil
						while !options.include?(option)
							print "[#{options.join(' ')}] ? "
							option = Action[gets.chomp]
						end
						option.apply(player, hand, deck)
						player_may_continue = option.may_continue?
					end
					puts hand.cards.to_s
					hand_index += 1
				end
			end
			
			#
			# Dealer plays
			#
			hand = dealer.hands[0]
			while (v = hand.value) && v < DealerHitLimit
				Action::Hit.apply(dealer, hand, deck)
			end
			puts "#{dealer.name} has: #{hand.cards}."
			
			#
			# Resolve bets
			#
			players.each do |player|
				next if !player.hands
				winnings = 0
				player.hands.each do |hand|
					if hand.natural?
						win = (hand.bet * 1.5).to_i
					elsif !hand.busted? && hand.value > dealer.hands[0].value.to_i
						win = hand.bet
					else
						win = -hand.bet
					end
					winnings += win
					player.bankroll += win + hand.bet
					discard.concat(hand.cards)
				end
				player.hands = nil
				puts player.name + ', you ' +
				case winnings <=> 0
				when -1
					"lost $#{-winnings}. "
				when 0
					"broke even. "
				when 1
					"won $#{winnings}. "
				end +
				"You have $#{player.bankroll}."
			end
			
			#
			# Repay loan shark
			#
			players.each_with_index do |player, index|
				debt = loan_shark[index] *= 1 + loan_shark.interest_rate
				print "#{player.name}, you owe me $#{debt.ceil}. How much can you repay me now? $"
				repayment = gets.to_i
				if repayment > 0 && repayment <= debt.ceil && repayment <= player.bankroll
					player.bankroll -= repayment
					loan_shark[index] -= repayment
					if debt < 0.01
						loan_shark[index] = 0
						puts "Bye. If you run out of cash again, you know where to come."
					else
						puts "Cheers. I hope I can see the rest of my money soon?"
					end
				end
			end
			
			#
			# Borrow from loan shark
			#
			players.find_all {|player| player.bankroll < table.min_bet }.each_with_index do |player, index|
				print "#{player.name}, you need cash to keep playing. Borrow some more! $"
				cash_out = gets.to_i
				if cash_out > loan_shark.max_loan - player.bankroll
					print "That's a bit rich for me. You can get up to $#{loan_shark.max_loan - player.bankroll}. How much? "
					cash_out = gets.to_i
					cash_out = loan_shark.max_loan if cash_out == 0
				end
				if cash_out > 0 && cash_out <= loan_shark.max_loan - player.bankroll
					player.bankroll += cash_out
					loan_shark[index] += cash_out
					puts "Good luck with that."
				end
			end
			
		end while players.find {|player| player.bankroll >= table.min_bet }
		puts "Oh, dear, no-one seems to have any money left."
	end
end

if $0 == __FILE__
	Blackjack::cli
end

