STAT 19000: Project 13 — Spring 2021

Motivation: We’d be remiss spending almost an entire semester solving data driven problems in python without covering the basics of classes. Whether or not you will ever choose to use this feature in your work, it is best to at least understand some of the basics so you can navigate libraries and other code that does use it.

Context: We’ve spent nearly the entire semester solving data driven problems in python, and now we are going to learn about one of the primary features in python: classes. Python is an object oriented programming language, and as such, much of python, and the libraries you use in python are objects which have properties and methods. In this project we will explore some of the terminology and syntax relating to classes.

Scope: python

Learning objectives
  • Explain the basics of object oriented programming, and what a class is.

  • Use classes to solve a data-driven problem.

Make sure to read about, and use the template found here, and the important information about projects submissions here.

Dataset

This is the continuation of the previous project. In this project we will learn about classes by simulating a simplified version of Blackjack! Don’t worry, while this may sound intimidating, there is very little coding involved, and much of the code will be provided to you to use!

Questions

Question 1

In the previous project, we built a Deck and Card class. What is one other very common task that people do with decks of cards? Shuffle! There is a function in Python called shuffle. It can be used like:

from random import shuffle
my_list = [1,2,3]
print(my_list)
shuffle(my_list)
print(my_list)

Run the Deck and Card code from the previous project. Create a Deck and try to shuffle it. What happens?

To fix this, we can implement the setitem dunder method. This dunder method allows us to "set" a value, much in the same way getitem allows us to "get" a value. Re-run your Deck class and try to shuffle again and print out the first couple of cards to ensure it is truly shuffled.

Items to submit
  • 1-2 sentences explaining what happens.

  • Python code used to solve the problem.

  • Output from running your code.

Question 2

Let’s take one last look at the Card class. In Blackjack, one thing you need to be able to do is count the value of the cards in your hand. Wouldn’t it be convenient if we were able to add Card objects together like the following?

print(Card("2", "clubs") + Card("k", "diamonds"))

In order to do this, implement the __add__ dunder method, and test out the following.

print(Card("2", "clubs") + Card("k", "diamonds")) # 15
print(Card("k", "hearts") + Card("q", "hearts")) # 25
print(Card("k", "diamonds") + Card("a", "spades") + Card("5", "hearts")) # what happens with this last example

What happens with the last example? The reason this happens is that the first 2 cards are added together without any issue. Then, we add the final card and everything breaks down. Any guesses why? The reason is that the result of adding the first 2 cards is an integer, 27, and we try to then add 27 + Card("5", "hearts") and Python doesn’t know what to do! As usual, there is a dunder method for that.

Implement __radd__ and try again. __radd__ will look nearly identical to __add__, but where you previously added together the result of 2 dictionary lookups, we now just add the plain argument to 1 dictionary lookup.

Items to submit
  • Python code used to solve the problem.

  • Output from running your code.

Question 3

Okay, rather than force you to stumble through writing a bunch of new code, we are going to provide you with a good amount of code, and have you read through it and digest it. We’ve provided:

  • A Card class with an updated __eq__ method, and updated card values to fit blackjack. Make sure to add your __add__ and __radd__ methods to the provided Card class. They are necessary to make everything work.

  • An updated Deck class with a draw method that allows the user to draw a card from the deck and keep track of how many cards are drawn. Make sure to add your __setitem__ and __getitem__ methods to the provided Deck class.

  • A Hand class that represents a "hand" of cards.

  • A Player class that represents a player.

  • A BlackJack class that represents a single game of Blackjack.

Card

Make sure to add your __add__ and __radd__ methods to the provided Card class. They are necessary to make everything work.

Methods from the previous project will be added below Saturday morning, or at the latest Monday morning.

class Card:
    _value_dict = {"2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8":8, "9":9, "10": 10, "j": 10, "q": 10, "k": 10, "a": 11}
    def __init__(self, number, suit):
        if str(number).lower() not in [str(num) for num in range(2, 11)] + list("jqka"):
            raise Exception("Number wasn't 2-10 or J, Q, K, or A.")
        else:
            self.number = str(number).lower()
        if suit.lower() not in ["clubs", "hearts", "diamonds", "spades"]:
            raise Exception("Suit wasn't one of: clubs, hearts, spades, or diamonds.")
        else:
            self.suit = suit.lower()

    def __str__(self):
        return(f'{self.number} of {self.suit.lower()}')

    def __repr__(self):
        return(f'Card(str({self.number}), "{self.suit}")')

    def __eq__(self, other):
        if isinstance(other, type(self)):
            if self.number == other.number:
                return True
            else:
                return False
        else:
            if self.number == other:
                return True
            else:
                return False

    def __lt__(self, other):
        if self._value_dict[self.number] < self._value_dict[other.number]:
            return True
        else:
            return False

    def __gt__(self, other):
        if self._value_dict[self.number] > self._value_dict[other.number]:
            return True
        else:
            return False


Deck

Make sure to add your __setitem__ and __getitem__ methods to the provided Deck class.

Methods from the previous project will be added below Saturday morning, or at the latest Monday morning.

class Deck:
    _suits = ["clubs", "hearts", "diamonds", "spades"]
    _numbers = [str(num) for num in range(2, 11)] + list("jqka")

    def __init__(self):
        self.cards = [Card(number, suit) for suit in self._suits for number in self._numbers]
        self._drawn = 0

    def __str__(self):
        return str(self.cards)

    def __len__(self):
        return len(self.cards) - self._drawn

    def draw(self, number_cards = 1):
        try:
            drawn_cards = self.cards[self._drawn:(self._drawn+number_cards)]
        except:
            print(f"Can't draw anymore cards, deck empty.")

        self._drawn += number_cards
        return drawn_cards


Hand

import queue
class Hand:
    def __init__(self, *cards):
        self.cards = [card for card in cards]

    def __str__(self):
        vals = [str(val) for val in self.cards]
        return(', '.join(vals))

    def __repr__(self):
        vals = [repr(val) for val in self.cards]
        return(', '.join(vals))

    def __len__(self):
        return len(self.cards)

    def __getitem__(self, key):
        return self.cards[key]

    def __setitem__(self, key, value):
        self.cards[key] = value

    def sum(self):
        # remember, when we compare to Ace of Hearts, we are really only comparing the values,
        # and ignoring the suit.
        number_aces = sum(1 for card in self.cards if card == Card("a", "hearts"))
        non_ace_sum = sum(card for card in self.cards if card != Card("a", "hearts"))

        if number_aces == 0:
            return non_ace_sum

        else:
            # only 2 options 1 ace is 11 the rest 1 or all 1
            high_option = non_ace_sum + number_aces*1 + 10
            low_option = non_ace_sum + number_aces*1

            if high_option <= 21:
                return high_option
            else:
                return low_option

    def add(self, *cards):
        self.cards = self.cards + list(cards)
        return self

    def clear(self):
        self.cards = []


Player

class Player:
    def __init__(self, name, strategy = None, dealer = False):
        self.name = name
        self.hand = Hand()
        self.dealer = dealer
        self.wins = 0
        self.draws = 0
        self.losses = 0
        if not self.dealer and not strategy:
            print(f"Non-dealer MUST have strategy.")

        self.strategy = strategy

    def __str__(self):
        summary = f'''{self.name}
------------
Wins: {self.wins/(self.wins+self.losses+self.draws):.2%}
Losses: {self.losses/(self.wins+self.losses+self.draws):.2%}
Draws: {self.draws/(self.wins+self.losses+self.draws):.2%}'''
        return summary

    def cards(self):
        if self.dealer:
            return [list(self.hand.cards)[0], "Face down"]
        else:
            return self.hand

BlackJack

import sys
class BlackJack:
    def __init__(self, *players, dealer = None):
        self.players = players
        self.deck = Deck()
        self.dealt = False
        if not dealer:
            self.dealer = Player('dealer', dealer=True)

    def deal(self):
        # shuffle the deck
        shuffle(self.deck)

        # we are ignoring dealing order and dealing to the dealer
        # first
        for _ in range(2):
            self.dealer.hand.add(*self.deck.draw())

        # deal 2 cards to each player
        for player in self.players:

            # first, clear out the players hands in case they've played already
            player.hand.clear()
            for _ in range(2):
                player.hand.add(*self.deck.draw())

        self.dealt = True

    def play(self):

        # make sure we've dealt
        if not self.dealt:
            sys.exit("You MUST deal the cards before playing.")

        # if dealer has face up ace or 10, checks to make sure
        # doesn't have blackjack.
        # remember, when we compare to Ace of Hearts, we are really only comparing the values,
        # and ignoring the suit.
        face_value_ten = (Card("10", "hearts"), Card("j", "hearts"), Card("q", "hearts"), Card("k", "hearts"), Card("a", "hearts"))
        if self.dealer.cards()[0] in face_value_ten:

            if self.dealer.hand.sum() == 21:
                # winners get a draw, losers
                # get a loss
                for player in self.players:
                    if player.hand.sum() == 21:
                        player.draws += 1
                    else:
                        player.losses += 1

                return "GAME OVER"

        # if the dealer doesn't win with a blackjack,
        # the players now know the dealer doesn't
        # have a blackjack


        # if the dealer doesn't have blackjack
        for player in self.players:
            # players play using their strategy until they hold
            while True:
                player_move = player.strategy(self, player)
                if player_move == "hit":
                    player.hand.add(*self.deck.draw())
                else:
                    break
        # dealer draws until >= 17
        while self.dealer.hand.sum() < 17:
            self.dealer.hand.add(*self.deck.draw())
        # if the dealer gets 21, players who get 21 draw
        # other lose
        if self.dealer.hand.sum() == 21:
            for player in self.players:
                if player.hand.sum() == 21:
                    player.draws += 1
                else:
                    player.losses += 1
        # otherwise, dealer has < 21, anyone with more wins, same draws,
        # and less loses
        elif self.dealer.hand.sum() < 21:
            for player in self.players:
                if player.hand.sum() > 21:
                    # player busts
                    player.losses += 1
                elif player.hand.sum() > self.dealer.hand.sum():
                    # player wins
                    player.wins += 1
                elif player.hand.sum() == self.dealer.hand.sum():
                    # player ties
                    player.draws += 1
                else:
                    # player loses
                    player.losses += 1
        # if dealer busts, players who didn't bust, win
        # players who busted, lose -- this is the house's edge
        else:
            for player in self.players:
                if player.hand.sum() < 21:
                    # player won
                    player.wins += 1
                else:
                    # player busted
                    player.losses += 1

        return "GAME OVER"

Read and understand the Hand class. Create a hand containing the: Ace of Diamonds, King of Hearts, Ace of Spades. Print the sum of the Hand. Add the 8 of Hearts to your Hand, and print the new sum. Do things appear to be working okay?

Items to submit
  • Python code used to solve the problem.

  • Output from running your code.

Question 4

If you take a look at the Player class and inside of the BlackJack class, you may notice something we refer to as a "strategy". We define a strategy as any function that accepts a BlackJack object, and a Player object, and returns either a str "hit" or a str "hold". Here are a couple examples of "strategies":

def always_hit_once(my_blackjack_game, me) -> str:
    """
    This is a simple strategy where the player
    always hits once.
    """
    if len(me.hand) == 3:
        return "hold"
    else:
        return "hit"
def seventeen_plus(my_blackjack_game, me) -> str:
    """
    This is a simple strategy where the player holds if the sum
    of cards is 17+, and hits otherwise.
    """
    if me.hand.sum() >= 17:
        return "hold"
    else:
        return "hit"

When you create a Player object, you provide a strategy as an argument, and that player uses the strategy inside of a BlackJack game.

Create 2 or more Player objects using any of the provided strategies. Create 1000 or more BlackJack games with those players, and play the games. Print the results for each player.

Items to submit
  • Python code used to solve the problem.

  • Output from running your code.

Question 5

Create your own strategy, make new games, and see how your strategy compares to the other provided strategies. Optionally, create a plot that illustrates the differences in the strategy.

Items to submit
  • Python code used to solve the problem.

  • Output from running your code.

  • (Optionally, 0 pts) The plot described.