STAT 19000: Project 12 — 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 third in a series of about 3 projects focused on reading and writing classes in Python.

Scope: Python, classes, composition vs. inheritance

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.

  • Identify when composition is being used or inheritance is being used.

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

Questions

Question 1

In this project, we are going to try something a little bit new. Instead of writing lots of code to generate data and work with cards, we will provide you with code and ask you to read, run, and understand code, and have some fun "simulating" card game information. The only reason we have projects based around classes in Python is because you run into them in the wild, and knowing how to navigate such code will benefit you greatly. It may sound tough, but I promise it is not bad at all, and if it is, post on Piazza and we will adjust or add more hints to help.

Read the introduction parts of this excellent article on composition and inheritance. Read enough of the sections to have a general idea about the difference between inheritance and composition — no need to read the entire article unless you are interested!

In your opinion, does the snippet of code provided below make use of techniques closer to inheritance or composition? Explain in 1-2 sentences why.

Both inheritance and composition can be extremely useful when coding. There are tradeoffs to both, however, composition will most likely be more useful in the data science world.

import random
from collections import Counter
import pandas as pd
import numpy as np

class Player:

    def __init__(self, name, strategy):
        self.name = name
        self.hand = []
        self.strategy = strategy

    def __str__(self):
        return self.name

    def draw(self):
        self.strategy.draw()

    def discard(self):
        self.strategy.discard()

    def can_end_game(self):
        return self.strategy.can_end_game(self)

    def should_end_game(self):
        return self.strategy.should_end_game(self)

    def make_move(self, game):
        return self.strategy.make_move(self, game)

    def get_best_hand(self):
        return self.strategy.get_best_hand(self)
Items to submit
  • Explain in 1-2 sentences why the snippet of code provided makes use of techniques closer to inheritance or composition.

Question 2

Below is the code you may use for this project. There is a lot to unpack, but this is okay, you don’t need a perfect understanding of it all to solve the questions in this project.

This code is not optimal at all. There are tons and tons of improvements that could be made both in design, documentation, readability, etc. It is however, a decent set of code to try and understand. It is inconsistent in comments and docstrings, which is really great to practice on!

It is also possible that there are bugs — if you find one, post in Piazza! Your instructors will be super pleased!

Recall in the previous question’s article that composition follows a "has a" relationship vs. inheritance’s "is a" relationship.

Take a peek at each of the classes in the code below, and identify 3 "has a" relationships between classes. One example would be a Game has a Deck. This is shown in the init function in Game where we set self.deck to be some provided deck object. Why is this pattern useful? Well, if for some reason we wanted to change our game to use a double deck (two decks of cards combined), this would be trivial. We could simply pass that an object that has twice the cards, and make sure that the methods that a regular Deck has are also implemented for the supposed DoubleDeck, and things should work fine!

Okay, list out 3 other "has a" relationships between classes.

import random
from collections import Counter
import pandas as pd
import numpy as np


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": 1}
    _gin_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": 1}
    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."


class Player:

    def __init__(self, name, strategy):
        self.name = name
        self.hand = []
        self.strategy = strategy

    def __str__(self):
        return self.name

    def draw(self):
        self.strategy.draw()

    def discard(self):
        self.strategy.discard()

    def can_end_game(self):
        return self.strategy.can_end_game(self)

    def should_end_game(self):
        return self.strategy.should_end_game(self)

    def make_move(self, game):
        return self.strategy.make_move(self, game)

    def get_best_hand(self):
        return self.strategy.get_best_hand(self)

    def hand_as_df(self, my_cards=None):
        if not my_cards:
            my_cards = self.hand

        data = {'suit': [], 'numeric_value': [], 'card': []}
        for card in my_cards:
            data['suit'].append(card.suit)
            data['numeric_value'].append(card._value_dict[card.number])
            data['card'].append(card)

        return pd.DataFrame(data=data)

    def get_sets(self, my_cards=None):

        if not my_cards:
            my_cards = self.hand

        def _flatten(t):
            return [item for sublist in t for item in sublist]

        def _get_cards_with_value(card_with_value, my_cards):
            return [card for card in my_cards if card == card_with_value]

        summarized = Counter(my_cards)
        sets = []
        for key, value in summarized.items():
            if value > 2:
                sets.append(_get_cards_with_value(key, my_cards))

        set_tuples = [(x._value_dict[x.number], x.suit) for x in _flatten(sets)]
        remaining_cards = list(filter(lambda x: (x._value_dict[x.number], x.suit) not in set_tuples, my_cards))

        return remaining_cards, sets

    def get_runs(self, my_cards=None):

        if not my_cards:
            my_cards = self.hand

        def _flatten(t):
            return [item for sublist in t for item in sublist]

        # get the hand as a pandas df
        df = self.hand_as_df(my_cards)

        # to store complete runs
        runs = []

        # loop through cards by suit
        for _, group in df.groupby("suit"):

            # sort the sub dataframe, group, by numeric value
            sorted_values = group.sort_values(["numeric_value"])

            # this is the key. create an auxilliary column that
            # is the difference between a column containing a count,
            # for example, 1, 2, 3, 4, 5, and the corresponding
            # numeric_values. This gives us a value that we can group by
            # containing all of the values in a run!
            sorted_values['aux'] = np.arange(len(sorted_values['numeric_value'])) - sorted_values['numeric_value']

            # sub groups here, subdf, will only contain runs now
            for _, subdf in sorted_values.groupby('aux'):

                # if the run is more than 2
                if subdf.shape[0] > 2:

                    # add the card objects to our list of lists
                    runs.append(subdf['card'].tolist())

        run_tuples = [(x._value_dict[x.number], x.suit) for x in _flatten(runs)]

        remaining_cards = list(filter(lambda x: (x._value_dict[x.number], x.suit) not in run_tuples, my_cards))

        return remaining_cards, runs


class Ruleset:

    @staticmethod
    def deal(game):
        """
        This implementation of deal we will deal
        10 cards each, alternating, starting
        with player1.

        Note: We are _not_ using our strategy to
        draw cards, but rather just drawing 10 cards
        each from the game's deck.
        """
        for _ in range(10):
            card = game.deck.cards.pop(0)
            game.player1.hand.append(card)

            card = game.deck.cards.pop(0)
            game.player2.hand.append(card)

    @staticmethod
    def first_move(game):
        """
        This implementation of first move
        will randomly choose a player to start,
        that player will draw, discard, etc.

        Afterwords, it will return two values. The
        first is a boolean indicating whether or not
        to end the game. The second is the player object.

        If the boolean indicates to end the game the player
        is the player ending the game, otherwise, it is
        the player whose turn is next.
        """
        player_to_start = random.choice((game.player1, game.player2))
        return player_to_start.make_move(game)


class Strategy:

    @staticmethod
    def get_best_hand(player):

        def _flatten(t):
            return [item for sublist in t for item in sublist]

        # this strategy is to get the runs then sets in that order,
        # count the remaining card values, then reverse the process,
        # get the sets then runs in that order, then count remaining
        # card values
        remaining_1 = player.hand
        remaining_1, runs1 = player.get_runs()
        remaining_1, sets1 = player.get_sets(remaining_1)

        remaining_card_value_1 = 0
        for card in remaining_1:
            remaining_card_value_1 += card._gin_value_dict[card.number]

        remaining_2 = player.hand
        remaining_2, sets2 = player.get_sets()
        remaining_2, runs2 = player.get_runs(remaining_2)

        remaining_card_value_2 = 0
        for card in remaining_2:
            remaining_card_value_2 += card._gin_value_dict[card.number]

        if remaining_card_value_1 <= remaining_card_value_2:
            return (remaining_1, _flatten(runs1 + sets1))
        else:
            return (remaining_2, _flatten(runs2 + sets2))

    @staticmethod
    def draw(player, game):
        # strategy to just always draw the face down card
        drawn_card = game.deck.cards.pop(0)
        player.hand.append(drawn_card)

    @staticmethod
    def discard(self, player, game):
        # strategy to discard the highest value card not
        # part of a set or a run

        # NOTE: This is a strategy that could be improved.
        # What if the highest value card is a king of spades,
        # and we also have another remaining card that is the
        # king of clubs?

        # NOTE: Another way to improve things would be using "deque"
        # https://docs.python.org/3/library/collections.html#collections.deque
        # prepending to a list is not efficient.
        remaining_cards, complete_cards = self.get_best_hand(player)
        remaining_cards = sorted(remaining_cards, reverse=True)

        to_discard = remaining_cards.pop(0)
        game.discard_pile.insert(0, to_discard)

        # remove from the player's hand
        for idx, card in enumerate(player.hand):
            if (card._value_dict[card.number], card.suit) == (to_discard._value_dict[to_discard.number], to_discard.suit):
                player.hand.pop(idx)

    @staticmethod
    def can_end_game(player):
        """
        The rules of gin (our version) state that in order to end the game
        the value of the non-set, non-run cards must be at most 10.
        """
        remaining_cards, _ = player.get_best_hand()

        remaining_value = 0
        for card in remaining_cards:
            remaining_value += card._gin_value_dict[card.number]

        return remaining_value <= 10

    @staticmethod
    def should_end_game(player):
        """
        Let's say our strategy is to knock as soon as possible.

        NOTE: Maybe a better strategy would be to knock as soon as
        possible if only so many turns have occurred?
        """

        if player.can_end_game():
            return True
        else:
            return False

    def make_move(self, player, game):
        """
        A move always consistents of the same operations.
        A players draws, discards, decides whether or not
        to end the game.

        This function returns two values. The first is a
        boolean value that says whether or not the game
        should be ended. The second is the player object
        of the individual playing the game. If the player
        is not ending the game, the player returned is the
        player whose turn it is now.
        """
        # first, we must draw a card
        self.draw(player, game)

        # then, we should discard
        self.discard(self, player, game)

        # next, we should see if we should end the game
        if player.should_end_game():
            # then, we end the game
            return True, player
        else:
            # otherwise, return the player with the next turn
            return False, (set(game.get_players()) - set((player,))).pop()


class Scorecard:
    def __init__(self, player1, player2):
        self.player1 = player1
        self.player2 = player2
        self.score = pd.DataFrame(data={"winner": [], f"points": []})

    def __str__(self):
        return f'{self.score.groupby("winner").sum()}'

    def stats(self):
        pass


class Game:
    def __init__(self, scorecard, deck, ruleset, player1, player2):
        self.scorecard = scorecard
        self.deck = deck
        self.discard_pile = []
        self.ruleset = ruleset
        self.player1 = player1
        self.player2 = player2

        # shuffle deck
        random.shuffle(self.deck)

    def get_players(self):
        return (self.player1, self.player2,)

    def play(self):
        """
        Play the game until a player ends the game.
        """
        # deal cards according to ruleset
        self.ruleset.deal(self)

        # first_move should bring the game's state
        # to a consistent state.

        # Example 1: use the rule where the most
        # recent loser deals 11 cards to the other player
        # and the other player begins by discarding 1 card

        # Example 2: use another variant of the "normal" rule where each player
        # is dealt 10 cards and then the remaining cards are
        # placed face down and the first card is flipped up
        # into the discard pile. A player is chosen at random
        # and they can start the game by drawing and then discarding
        end_game, player = self.ruleset.first_move(self)

        if end_game:
            self.end_game(player)

        while not end_game:
            if len(self.deck.cards) <= 2:
                # reset game in draw
                self.reset_game()

            end_game, player = player.make_move(self)

        self.end_game(player)


    def end_game(self, game_ender):
        """
        Ending a game involves the following process:

        1. If the player ending the game if "going gin", that player
        gets 25 points plus the value of the other players remaining
        cards.
        2. The other player can add their remaining cards to any of the game ender's sets or runs.
        3. Now, the value of the remaining cards for the player
        ending the game are compared to those of the other player,
        after the other player has potentially reduced their remaining
        cards in step 2.
        4. If the player ending the game has strictly fewer points,
        the player ending the game receives the difference between
        their remaining cards and the other players remaining cards.
        5. If the player ending the game has equal to or more points,
        the player ending the game has been undercut. The other player
        receives 25 points plus the difference between their remaining
        cards and the other players remaining cards.
        """

        def _flatten(t):
            return [item for sublist in t for item in sublist]

        def _get_rid_of_deadwood(game_ender, other_player):
            remaining_cards, complete_cards = game_ender.get_best_hand()
            other_remaining, other_complete = other_player.get_best_hand()

            combined_remaining1 = other_remaining + complete_cards
            combined_remaining1, runs1 = other_player.get_runs(combined_remaining1)
            combined_remaining1, sets1 = other_player.get_sets(combined_remaining1)

            combined_remaining2 = other_remaining + complete_cards
            combined_remaining2, runs2 = other_player.get_runs(combined_remaining2)
            combined_remaining2, sets2 = other_player.get_sets(combined_remaining2)

            remaining_card_value_1 = 0
            for card in combined_remaining1:
                remaining_card_value_1 += card._gin_value_dict[card.number]

            remaining_card_value_2 = 0
            for card in combined_remaining2:
                remaining_card_value_2 += card._gin_value_dict[card.number]

            if remaining_card_value_1 <= remaining_card_value_2:
                # remove the cards used in a set or run from other_remaining
                melds = [(x._value_dict[x.number], x.suit) for x in _flatten(runs1) + _flatten(sets1)]
                updated_other_remaining = list(filter(lambda x: (x._value_dict[x.number], x.suit) not in melds, other_remaining))
                return updated_other_remaining
            else:
                melds = [(x._value_dict[x.number], x.suit) for x in _flatten(runs1) + _flatten(sets1)]
                updated_other_remaining = list(filter(lambda x: (x._value_dict[x.number], x.suit) not in melds, other_remaining))
                return updated_other_remaining

        # get the "other player"
        other_player = (set(self.get_players()) - set((game_ender,))).pop()

        # get both players best hands
        remaining_cards, complete_cards = game_ender.get_best_hand()
        other_remaining, other_complete = other_player.get_best_hand()

        # is the game ender "going gin"?
        if not remaining_cards:
            winner = game_ender
            points = 25
            for card in other_remaining:
                points += card._gin_value_dict[card.number]

        else:
            # let the other_player play any deadwood/remaining cards
            # they have on the game ender's sets/runs
            other_remaining = _get_rid_of_deadwood(game_ender, other_player)

            # compare deadwood
            enders_deadwood = 0
            for card in remaining_cards:
                enders_deadwood += card._gin_value_dict[card.number]

            other_deadwood = 0
            for card in other_remaining:
                other_deadwood += card._gin_value_dict[card.number]

            if enders_deadwood < other_deadwood:
                winner = game_ender
                points = other_deadwood - enders_deadwood
            else:
                winner = other_player
                points = 25 + (enders_deadwood - other_deadwood)

        # tally score
        self.scorecard.score = self.scorecard.score.append({"winner": str(winner), "points": points}, ignore_index=True)

        # get a fresh shuffled deck and clear out hands
        self.reset_game()

    def reset_game(self):
        # get a fresh shuffled deck and clear out hands
        self.deck = Deck()
        self.discard_pile = []
        self.player1.hand = []
        self.player2.hand = []
Items to submit
  • Identify 3 "has a" relationships between classes, and give a brief explanation (say, 1 sentence) about each of these 3 "has a" relationships.

Question 3

Use the provided code to create the following objects:

  • A Strategy object that player1 and player2 (see below) will use.

  • A Deck object for the game.

  • A Ruleset object for the game.

  • A Player object called player1 that represents the first player.

  • A Player object called player2 that represents the second player.

  • A Scorecard object for the game between these two players.

  • A Game object that uses the objects you’ve created.

Once you have your Game created, go ahead and play a game using the play method! After you’ve played a game, print the Scorecard object you created. Typically Gin is played over and over until one player gets 100 points. Play another game using the play method. Print the Scorecard object again — did it change as you would expect?

Items to submit
  • Show the game play as you test the code.

Question 4

Typically, the way Gin works is you would play a "game" with the other player. The winner would get points. These points are tracked until the first player gets to 100 points. Once that happens, the winner would get a single "set point". You could then track these "set points" over many days/months/years to keep track of who wins the most, etc. Or, you could agree to play until the first person gets to 3 (or any other arbitrary rule).

If you were to play many games of Gin from the previous question, you would notice that the scorecard would just grow and grow. Currently there is not logic added that keeps track of whether or a player has won a set, winning a "set point".

Write code that simulates a game of Gin that goes until one of the players gets to 3 "set points". Print the final Scorecard after each won "set point". Make sure to create a fresh game with a fresh Scorecard between each won "set point" (or, if you have another way you’d like to tackle this problem, feel free!). At the end of the simulation, print the final score, for example:

#...
print(scorecard)
#         points
# winner
# David     26.0
# Kali      50.0
#...
# code to print final score...
# Final score:
# David: 2
# Kali: 3

This definition of game_over might be useful for your work:

def game_over(scorecard):
    winning_scoreboard = scorecard.score.groupby("winner").sum().reset_index().loc[scorecard.score.groupby("winner").sum().reset_index()['points'] >= 100.0, :]
    return winning_scoreboard['winner'], winning_scoreboard.shape[0] > 0.0

You can access the scorecard as a dataframe by scorecard.score.

Items to submit
  • Show the game play as you test the code.

Question 5

Composition allows us to do one very powerful thing with the code that we’ve written — it allows us to quickly adopt and test out different playing strategies. The following is the Strategy we provided for you.

class Strategy:

    @staticmethod
    def get_best_hand(player):

        def _flatten(t):
            return [item for sublist in t for item in sublist]

        # this strategy is to get the runs then sets in that order,
        # count the remaining card values, then reverse the process,
        # get the sets then runs in that order, then count remaining
        # card values
        remaining_1 = player.hand
        remaining_1, runs1 = player.get_runs()
        remaining_1, sets1 = player.get_sets(remaining_1)

        remaining_card_value_1 = 0
        for card in remaining_1:
            remaining_card_value_1 += card._gin_value_dict[card.number]

        remaining_2 = player.hand
        remaining_2, sets2 = player.get_sets()
        remaining_2, runs2 = player.get_runs(remaining_2)

        remaining_card_value_2 = 0
        for card in remaining_2:
            remaining_card_value_2 += card._gin_value_dict[card.number]

        if remaining_card_value_1 <= remaining_card_value_2:
            return (remaining_1, _flatten(runs1 + sets1))
        else:
            return (remaining_2, _flatten(runs2 + sets2))

    @staticmethod
    def draw(player, game):
        # strategy to just always draw the face down card
        drawn_card = game.deck.cards.pop(0)
        player.hand.append(drawn_card)

    @staticmethod
    def discard(self, player, game):
        # strategy to discard the highest value card not
        # part of a set or a run

        # NOTE: This is a strategy that could be improved.
        # What if the highest value card is a king of spades,
        # and we also have another remaining card that is the
        # king of clubs?

        # NOTE: Another way to improve things would be using "deque"
        # https://docs.python.org/3/library/collections.html#collections.deque
        # prepending to a list is not efficient.
        remaining_cards, complete_cards = self.get_best_hand(player)
        remaining_cards = sorted(remaining_cards, reverse=True)

        to_discard = remaining_cards.pop(0)
        game.discard_pile.insert(0, to_discard)

        # remove from the player's hand
        for idx, card in enumerate(player.hand):
            if (card._value_dict[card.number], card.suit) == (to_discard._value_dict[to_discard.number], to_discard.suit):
                player.hand.pop(idx)

    @staticmethod
    def can_end_game(player):
        """
        The rules of gin (our version) state that in order to end the game
        the value of the non-set, non-run cards must be at most 10.
        """
        remaining_cards, _ = player.get_best_hand()

        remaining_value = 0
        for card in remaining_cards:
            remaining_value += card._gin_value_dict[card.number]

        return remaining_value <= 10

    @staticmethod
    def should_end_game(player):
        """
        Let's say our strategy is to knock as soon as possible.

        NOTE: Maybe a better strategy would be to knock as soon as
        possible if only so many turns have occurred?
        """

        if player.can_end_game():
            return True
        else:
            return False

    def make_move(self, player, game):
        """
        A move always consistents of the same operations.
        A players draws, discards, decides whether or not
        to end the game.

        This function returns two values. The first is a
        boolean value that says whether or not the game
        should be ended. The second is the player object
        of the individual playing the game. If the player
        is not ending the game, the player returned is the
        player whose turn it is now.
        """
        # first, we must draw a card
        self.draw(player, game)

        # then, we should discard
        self.discard(self, player, game)

        # next, we should see if we should end the game
        if player.should_end_game():
            # then, we end the game
            return True, player
        else:
            # otherwise, return the player with the next turn
            return False, (set(game.get_players()) - set((player,))).pop()

Copy and paste the code above to create your own class MyStrategy. Modify the code in MyStrategy to do something different. Try to not modify the method arguments or return types, as this will cause the need for more modification. Here are some examples of changes you could make:

  • Modify the draw method to check if the top card (card at index 0 of the discard_pile) would create a new set or run, and if so, choose to draw from the discard_pile instead of the deck.

  • Modify the discard method to not discard a partial set or run — a set or run of two cards, where you just need 1 more to complete it.

  • Modify the should_end_game method to only end the game if the player has "deadwood" under a certain value.

  • Modify the should_end_game method to only end the game if the player has 0 "deadwood" (i.e. if they have Gin, or can "go Gin").

  • Modify the strategy to give a player perfect memory — i.e. they can remember all of the discard_pile, and use this to change the strategy (harder).

This is really cool, because you could test out, computationally, many different strategies to see what increases your odds of winning! For now, simulate a full game (like in the previous question) where one player has the default strategy, and the other has your new MyStrategy. Did the player with your new strategy end up winning? In the next project we will experiment more with your new strategy.

Gin is not hard to learn, and it is a game of skill (meaning the odds of winning are not the same for someone with skill as someone without skill, not a game of purely luck.

This site has a pretty short 1 page explanation of the rules. Here is a quick breakdown of the version we’ve implemented.

  • Each player is dealt 10 cards.

  • A random player is chosen to start the game.

  • The first player makes their move. With the default strategy, the player draws the facedown card from the Deck.

  • The player discards a card face up in the discard_pile.

  • The next player draws a card from either the deck or the discard_pile — the default strategy is to always draw from the deck.

  • The player then discards a card.

  • This repeats until a player decides to end the game.

  • A player can end the game by knocking or going gin.

  • In order to "go gin", a player must be able to make full sets and/or runs from all 10 cards (note that the 11th card is always discarded).

  • If a player goes gin, they get 25 points plus the value of the opponent’s cards that don’t belong to a run or a set.

  • Otherwise a player may choose to end the game by knocking.

  • In order to knock, a player must have cards with total value less than or equal to 10 points that are not a part of a set or run (again, with a final of only 10 cards — we always discard the 11th card before ending the game or at the end of each turn). All other cards must be a part of a set or run. These "remaining cards", or cards that are not a part of a set or a run are called "deadwood".

  • The opponent gets the opportunity to add their deadwood onto the knocking player’s complete runs or sets. Any deadwood added to the knocker’s runs and/or sets is no longer deadwood.

  • If the player that knocks has a total value of deadwood less than (strictly) than the total value of the opponent’s deadwood, they win the amount of points in the difference between the total value of their deadwood and their opponent’s.

  • If the player that knocks has equal to or greater total value of deadwood than their opponent, the knocker got undercut. The opponent then wins 25 points plus the difference between their deadwood and the knockers.

  • The first player to get to 100 points wins a "set point".

  • Rinse and repeat until a player has 3 "set points" or until some other predetermined criteria is met.

Items to submit
  • Show your modified strategy, discuss what you changed, and show how it works.

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.