"""
 blackjack.py

 Implement a blackjack game with objects Card and Deck,
 from Zelle exercise 11 from chapter 10 
 and exercise 15 from chapter 11

 A homework assignment for the Intro CS course.

 Based on Jim's card.py and deck.py at 
 cs.bennington.college/courses/spring2021/introcs/code/objects/deck.py

 Jim Mahoney | cs.bennington.college | May 2021 | MIT License
"""

import random

suits = ('c', 'd', 'h', 's')
ranks = tuple(range(1, 14))   # (1, 2, 3, ...., 11, 12, 13)
suitnames = {'c': 'Clubs', 'd': 'Diamonds', 'h': 'Hearts', 's': 'Spades'}
ranknames = (None, 'Ace', 'Two', 'Three', 'Four', 'Five', 'Six',
             'Seven', 'Eight', 'Nine', 'Ten', 'Jack', 'Queen', 'King')
             
class Card:
    """ A playing card """
    def __init__(self, rank=None, suit=None):
        if rank == None:
            rank = random.choice(ranks)
        if suit == None:
            suit = random.choice(suits)
        self.rank = rank
        self.suit = suit
    def __str__(self):
        return f"{ranknames[self.rank]} of {suitnames[self.suit]}"
    def blackjack_value(self):
        """ Return blackjack card value, i.e. 10 for Jack,Queen,King """
        # An ace can be 1 or 11. But here we just treat it as 1,
        # and consider the 11 choice in class Hand, since it depends
        # on the other cards in the hand.
        if self.rank >= 10:
            return 10
        else:
            return self.rank

class Deck:
    """ A deck of 52 cards """
    def __init__(self):
        self.cards = []
        for suit in suits:
            for rank in ranks:
                self.cards.append(Card(rank, suit))
    def deal_card(self):
        """ Return and remove the top card """
        return self.cards.pop()
    def shuffle(self):
        """ shuffle cards in place """
        random.shuffle(self.cards)
    def cards_left(self):
        """ Return number of cards remaining """
        return len(self.cards)

class Hand:
    """ A hand of BlackJack cards """
    
    def __init__(self):
        self.cards = []

    def __str__(self):
        card_strings = []
        for card in self.cards:
            card_strings.append(str(card))
        return ', '.join(card_strings) + ' : ' + str(self.value())
        
    def hit_me(self, deck):
        """ Deal a card from the deck into this hand. """
        self.cards.append(deck.deal_card())
        
    def value(self):
        """ Return the blackjack value of this hand. """
        # Note that an ace can be either 1 or 11.
        total = 0
        # First add up all the values, treating the aces as 1.
        for card in self.cards:
            total = total + card.blackjack_value()
        # Now for each ace, add 10 to the score if that doesn't go over 21.
        for card in self.cards:
            if card.rank == 1:          # Ace ?
                if total + 10 <= 21:    # Can this ace be 11 without busting?
                    total = total + 10  # Then make it so.
        return total

    def is_bust(self):
        """ Return True if the hand is over 21 """
        return self.value() > 21

    def is_blackjack(self):
        """ Return True if the had is exactly 21 """
        return self.value() == 21

class Dealer(Hand):
    def another_card(self):
        """ Return True if the dealer wants a card. """
        # Casino rules are that the dealer takes cards
        # until the hand is more than 17.
        return self.value() < 17

class Player(Hand):
    def another_card(self):
        """ Ask if the user wants a card; return True or False """
        choice = input("Do you want another card? (yes or no) ")
        return choice.lower()[0] == 'y'
    
class Game:
    """ A simplistic game of BlackJack with a dealer and player """
    # No splits, no betting - just the basics.

    def __init__(self):
        self.deck = Deck()
        self.deck.shuffle()
        self.player = Player()
        self.dealer = Dealer()

    def play(self):
        print("** Blackjack **")

        # deal two cards to the player.
        self.player.hit_me(self.deck)
        self.player.hit_me(self.deck)

        # continue to deal cards to the player
        # until they win, lose, or don't want any more.
        while True:
            print("Your hand is ", str(self.player))
            if self.player.is_bust():
                print("Oops - busted. You lose.")
                return
            elif self.player.is_blackjack():
                print("Blackjack - you win!")
                return
            elif self.player.another_card():
                self.player.hit_me(self.deck)
            else:
                break

        # Now it's the dealer's turn.
        # Deal cards until they don't want anymore.
        while self.dealer.another_card():
            self.dealer.hit_me(self.deck)

        # And see how it turned out.
        print("Dealer's hand is ", str(self.dealer))
        if self.dealer.is_bust():
            print("Dealer is over 21 ... you win.")
        elif self.player.value() > self.player.value():
            print("Your hand is better than the dealer's : you win.")
        else:
            print("Your hand doesn't beat the dealer's : you lose.")
        
def main():
    Game().play()

if __name__ == '__main__':
    main()