STAT 19000: Project 11 — Spring 2022

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 using 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 attributes and methods. In this project we will explore some of the terminology and syntax relating to classes. This is the second in a series of 3 projects focused on reading and writing classes in Python.

Scope: Python, classes

Learning Objectives
  • Use classes to solve a data-driven problem.

  • Understand and identify attributes and methods of a class.

  • Differentiate between class attributes and instance attributes.

  • Differentiate between instance methods, class methods, and static methods.

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

Questions

Question 1

from collections import Counter

class Player:
    def __init__(self, name, deck):
        self.name = name
        self.deck = deck
        self.hand = []

    def __str__(self):
        return(f"""
        {self.name}\n
        Top 5 cards: {self.deck[:5]}
        """)

    def draw(self):
        card = self.deck.cards.pop(0)
        self.hand.append(card)

    def has_set(self):
        summarizedhand = Counter(self.hand)
        for key, value in summarizedhand.items():
            if value >= 3:
                return True
        return False


class Card:
    _value_dict = {"2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8":8, "9":9, "10": 10, "j": 11, "q": 12, "k": 13, "a": 14}
    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 self.number == other.number:
            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

    def __hash__(self):
        return hash(self.number)


class Deck:
    brand = "Bicycle"
    _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]

    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 __str__(self):
        return f"A {self.brand.lower()} deck."

Recall from the previous project the following.

Two common patterns that are important to be able to quickly recognize in many gin rummy games are sets and runs.

A set is a group of cards with different suits but the same value. In order to qualify as a set, there must be 3 or more cards.

A run is a group of cards with the same suit with sequential values. In order to qualify as a run, there must be 3 or more cards.

In the final question from the previous project we wrote a method (a function for a class) called has_set which returned True if the given Player had a set or not. This is useful, sure, but not as useful as it could be!

Write another method called get_sets which returns a list of lists, where each nested list contains the cards of a complete set. The results should look something like the following, feel free to run the code many times to see if it looks as if it is working.

import random

deck = Deck()
player1 = Player("Alice", deck)
random.shuffle(deck)
for _ in range(20):
    player1.draw()

sets = player1.get_sets()
sets
output
[[Card(str(5), "clubs"), Card(str(5), "spades"), Card(str(5), "hearts")],
 [Card(str(6), "diamonds"), Card(str(6), "clubs"), Card(str(6), "spades")]]
Items to submit
  • Code used to solve this problem.

  • Output from running the code.

Question 2

Runs are a bit more complicated to figure out than sets. In order to make things slightly easier, let’s write a method called hand_as_df that takes a player’s hand and converts it into a pandas dataframe with the following columns: suit, numeric_value, card. The first column is just a column with the strings: "spades", "hearts", "diamonds", or "clubs". The second is the numeric value of a given card: 1 through 13.

You may want to change your Card class so that the value isn’t 2-14 but 1-13, where ace is low (1) and only low.

The final column is the Card object itself!

The following should result in a dataframe.

import random

deck = Deck()
player1 = Player("Alice", deck)
random.shuffle(deck)
for _ in range(20):
    player1.draw()

sets = player1.hand_as_df()
sets
Items to submit
  • Code used to solve this problem.

  • Output from running the code.

Question 3

Okay, now for the more challenging part. Write a method called get_runs that returns a list of lists where each list contains the cards of the given run. Note that runs of more than 3 should be in the same list. If a run is 6 or more, it should be represented in a single list, not 2 lists of 3 or more.

You can run the following code until you can see that your method is working as intended.

import random

deck = Deck()
player1 = Player("Alice", deck)
random.shuffle(deck)
for _ in range(20):
    player1.draw()

runs = player1.get_runs()
runs
example output
[[Card(str(j), "hearts"), Card(str(q), "hearts"), Card(str(k), "hearts")],
 [Card(str(a), "spades"),
  Card(str(2), "spades"),
  Card(str(3), "spades"),
  Card(str(4), "spades"),
  Card(str(5), "spades")]]

Since this question is more challenging than normal, this is the last question. Try to solve this puzzle before looking at the tips below!

Grouping by suit would be a good way to isolate cards of a certain suit. Remember runs can only be with cards of the same suit.

To group by suit and loop through the groups, you can use the groupby method.

for idx, group in my_df.groupby("suit"):
    print(idx) # an index
    print(group) # a dataframe with only cards from the same suit
    print(group.shape) # note that all the regular data frame methods are available to use

Think about the following values. Consider the numeric_value column, and consider how useful the difference column is in our situation. Maybe we could do something with that?

values
some_column, numeric_value, difference
1, 1, 0
2, 2, 0
3, 3, 0
4, 5, -1
5, 6, -1
6, 8, -2
7, 9, -2
7, 9, -2
Items to submit
  • Code used to solve this problem.

  • Output from running the code.

Please make sure to double check that your submission is complete, and contains all of your code and output before submitting. If you are on a spotty internet connect ion, it is recommended to download your submission after submitting it to make sure what you think you submitted, was what you actually submitted.

In addition, please review our submission guidelines before submitting your project.