Monday, 2 February 2015

Entry #2: Creating A Lottery Ticket Subclass And Superclass

Recently we had learned about creating superclasses and subclasses and implementing them into the course. I decided that I wanted to demonstrate my understanding of this by making a lottery ticket using this approach. First I had to consider the barebones of any lottery ticket. These barebones would be essential in implementing the superclass. The idea behind a lottery ticket is that it contains information which based on certain rules will give out a prize. A ticket tends to be "empty" until it is revealed. Once it is revealed, then the prize is determined. 

This means:


  • A lottery ticket needs contents and a place to hold the prize
  • A lottery ticket starts off empty or "not revealed"
  • When a lottery ticket is revealed, the contents are set on the ticket
  • Having the contents, one can then determine the prize. 

So with that in mind, I create my lottery ticket superclass:


class LotteryTicket():
    """ A class to represent a lottery ticket """
    
    def __init__(self):
        """ (LotteryTicket) -> NoneType
        
        Initialize a lottery ticket to contain contents and prize
        
        >>> lot = LotteryTicket()
        >>> isinstance(lot, LotteryTicket)
        True
        >>> lot.contents
        ''
        >>> lot.prize
        0
        """
        
        self.contents = ''
        self.prize = 0

Here, it is noted that contents and prize are set right from the get-go, though since we are not sure what the contents will be, we leave it as a blank string. Prize is also set to 0 since we are not aware of what it will be based on the contents.
    
    def __str__(self):
        """ (LotteryTicket) -> str
        
        Return a string representation of LotteryTicket
        
        >>> lot = LotteryTicket()
        >>> print(lot)
        LotteryTicket
        Contents: 
        Prize: 0
        """
        
        return "LotteryTicket\nContents: {0}\nPrize: {1}".format(self.contents, self.prize)

I've defined the __str__ method more to give an idea of what a generic ticket should look like. 
    
    def __repr__(self):
        """ (LotteryTicket) -> str
        
        Return a shell representation of LotteryTicket
        
        """
        
        raise NotImplementedError('Subclass needed')
    
    
    def __eq__(self, other):
        """ (LotteryTicket, LotteryTicket) -> bool
        
        Return whether two LotteryTickets are the same
        
        """
        
        raise NotImplementedError('Subclass needed')
    
    def set_ticket(self):
        """ (LotteryTicket) -> NoneType
        
        Set the contents of LotteryTicket
        
        """
        
        raise NotImplementedError('Subclass needed')
    
    def get_prize(self):
        """ (LotteryTicket) -> NoneType
        
        Obtain prize based on the contents of LotteryTicket
        
        """
        
        raise NotImplementedError('Subclass needed')

All of these methods have not been implemented but they are crucial to the skeleton of a lottery ticket so they've been included for the sake of reference and convenience. 

Now that I have a basic representation of a generic lottery ticket dubbed LotteryTicket, I can now start to think of making more specific LotteryTickets. There are many ways I can go about making more specific lottery tickets and I have even managed to come up with a few. But for the sake of convenience, I'm going to pool all of these ideas into one LotteryTicket subclass I'll call MegaWinningsTicket. 

from LotteryTicket import LotteryTicket

import random

class MegaWinningsTicket(LotteryTicket):
    """ A lottery ticket with Mega Winnings! 
    
    Price of MegaWinningsTicket determine which game you play!

    5 - Pick 2
    Try to get the winning two numbers!
    10 - Pick 3
    Try to get the winning three numbers!
    15 - Crossword
    Try to get the necessary letters to complete the crossword!
    20 - Golden Diamond

    Try to get the elusive Golden Diamond!
    """
    
    def __init__(self, price):
        """ (MegaWinningsTicket) -> NoneType
        
        Precondition: price must be in [5, 10, 15, 20]
            
        Initialize a MegaWinnings lottery ticket to contain contents and prize
        and to hold the value of price
            
        >>> mega = MegaWinningsTicket(5)
        >>> isinstance(mega, MegaWinningsTicket)
        True
        >>> mega.contents
        ''
        >>> mega.prize
        0
        >>> mega.price
        5
        """
            
        LotteryTicket.__init__(self)

        self.price = price

Notice that now there is an added parameter in MegaWinningsTicket dubbed price. While it may not be advisable to name this parameter price since it is so close to prize, this little bit allows me to extend the __init__ method to suit this subclass. Specifically, the price allows you to play a certain game that the MegaWinningsTicket can support. Let's go through each of them in order.


  • Pick 2
    Price: 5
    Contents: 2 numbers
    Objective: Obtain two numbers that are exactly the same to the results
    Prize: 2500 for both numbers
    Odds of winning the big prize: 1 in 100
  • Pick 3
    Price: 10
    Contents: 3 numbers
    Objective: Obtain three numbers that are exactly the same to the results
    Prize: 10000 for complete match (big prize), 5000 for two numbers that match
    Odds of winning the big prize: 1 in 1000
  • Crossword
    Price: 15
    Contents: 10 letters
    Objective: To get all the words in this crossword

      r
    c a b
      r
    b e l l
    a     a
    r     b
    e r r
    Prize: 50000 for all words removed (big prize), 100 for 5 words removed, 50 for 4 words removed, 25 for 3 words removed, 10 for 2 words removed, 2 for one removed
    Odds of winnings the big prize: 1 in 25294 (5311735 possible combinations / 210 that contain the letters c, a, b, r, e, l)
  • Golden Diamond
    Price: 20
    Contents: One of the following objects: Golden Diamond, Diamond, Gold, Zirconium, Pyrite, Gold-Painted Rock, Shiny Rock
    Objective: Obtain a golden diamond
    Prize: 10000000 for a golden diamond (big prize), 500000 for a diamond, 100000 for gold, 1000 for zirconium, 20 for pyrite
    Odds of winning the big prize:
    1 in 111111 (1 Golden Diamond + 10 Diamonds + 100 Gold + 1000 Zirconium + 10000 Pyrite + 50000 Gold-Painted Rocks + 50000 Shiny Rocks)
Each of these will be shown in more detail but one can see that there will have to be an implementation of __eq__ that is specific to this function 

def __str__(self):
        """ (MegaWinningsTicket) -> str
               
        Return a string representation of MegaWinningsTicket
               
        >>> mega = MegaWinningsTicket(5)
        >>> print(mega)
        MegaWinningsTicket
        Contents: 
        Prize: 0
        Price: 5
        """
        
        return "MegaWinningsTicket\nContents: {0}\nPrize: {1}\nPrice: {2}".format(self.contents, self.prize, self.price)

For the __str__ method, I overwrote the method so that it would also include the price in the string.
    
    def __repr__(self):
        """ (MegaWinningsTicket) -> str
           
        Return a shell representation of MegaWinningsTicket
        
        >>> mega = MegaWinningsTicket(5)
        >>> mega
        MegaWinningsTicket(, 0, 5)
        """    
        
        return "MegaWinningsTicket({0}, {1}, {2})".format(self.contents, self.prize, self.price)

I have implemented the __repr__ method more as practice in doing so since there is no purpose it serves in the code.

def __eq__(self, other):
        """ (MegaWinningsTicket, MegaWinningsTicket) -> bool
        
        Return whether two MegaWinningsTickets are the same
        
        >>> mega = MegaWinningsTicket(5)
        >>> winnings = MegaWinningsTicket(5)
        >>> ticket = MegaWinningsTicket(10)
        >>> mega == winnings
        True
        >>> mega == ticket
        False
        >>> mega.contents = '3'
        >>> mega == winnings
        False
        >>> winnings.contents = '3'
        >>> winnings.prize = 20
        >>> mega == winnings
        True
        """
        
        return self.contents == other.contents and self.price == other.price

Here, I've made it clear that contents have to be the same as well as price (which isn't necessary in the following code, but asserts that the tickets belong to the same game). This brings up a question as to why I didn't have this implemented in the LotteryTicket superclass:

return self.contents == other.contents

Well that is because the way that certain tickets and games conduct themselves may rely on checking for equality in different ways, which may be a matter of parameters that the LotteryTicket subclass contains or what is being checked in contents.

 def set_ticket(self):
        """ (MegaWinningsTicket) -> NoneType
        
        Set the contents of MegaWinningsTicket based on price
        
        """
        
        number_list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        
        if self.price == 5:  # Pick 2
            self.contents = [random.choice(number_list), random.choice(number_list)]
            
        if self.price == 10: # Pick 3
            self.contents = [random.choice(number_list), random.choice(number_list), random.choice(number_list)]
            
        if self.price == 15: # Crossword
            alphabet = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
            words = ['cab', 'bell', 'rare', 'err', 'bare', 'lab']
            print('The words are: {}'.format(words))
            letters = []
            
            for i in range(10):
                letter = random.choice(alphabet)
                letters.append(letter)
                alphabet.remove(letter)
                
            print('Your letters are: {}'.format(letters))
            new_words = []
            
            for word in words:
                for letter in letters:
                    word = word.replace(letter, '')
                new_words.append(word)
                
            print(new_words)
            self.contents = new_words.count('')
            
        if self.price == 20: #Golden Diamond
            
            gem_list = ['Golden Diamond'] + ['Diamond'] * 10 + ['Gold'] * 100 + ['Zirconium'] * 1000 + ['Pyrite'] * 10000 + ['Gold-Painted Rock'] * 50000 + ['Shiny Rock'] * 50000
            self.contents = random.choice(gem_list)

Now this is where we get to the implementation of an important part of the LotteryTicket subclass, that being the set_ticket method which "reveals" the ticket by setting the contents depending on the price. For Pick 2, a list of two numbers is set. For Pick 3, a list of three numbers is set. For Crossword, the amount of words removed is set. And lastly, for Golden Diamond, a string is set. Having contents set will then help us to figure out how to get the prize.
            
    def get_prize(self):
        """ (MegaWinningsTicket) -> NoneType
        
        Precondition: self.price == 15 or self.price == 20 and MegaWinningsTicket
        has been given contents by the set_ticket method
        
        Obtain prize based on the contents of MegaWinningsTicket
        
        """
        
        if self.price == 15:
            possible_winnings = [0, 2, 10, 25, 50, 100, 50000]
            self.prize = possible_winnings[self.contents]
        
        if self.price == 20:
            possible_winnings = {'Gold Diamond': 10000000, 'Diamond': 500000, 'Gold': 100000, 'Zirconium': 1000, 'Pyrite': 20, 'Gold-Painted Rock': 0, 'Shiny Rock': 0}
            self.prize = possible_winnings[self.contents]

Notice how in the docstring, it specifically indicates that set_ticket must have been run on the MegaWinningsTicket for get_prize to properly work. For Crossword, the contents is used as an index on the list possible_winnings. For example, if 2 words were removed, 2 is the contents. And if 2 is the contents, the prize is possible_winnings[2] which equals 10, thus getting the prize and setting it. Golden Diamond works similarly with the contents serving as a key in the dict possible_winnings. These approaches do not rely on another ticket as the results, so therefore Pick 2 and Pick 3 need their own method
    
    def pick_prize(self, results):
        """ (MegaWinningsTicket, MegaWinningsTicket) -> NoneType
        
        Precondition: self.price == 5 or self.price == 10 and both MegaWinningsTickets
        has been given contents by the set_ticket method
        
        Obtain prize based on how well the ticket compares with results
        
        >>> mega = MegaWinningsTicket(5)
        >>> results = MegaWinningsTicket(5)
        >>> mega.contents = [4, 2]
        >>> results.contents = [4, 2]
        >>> mega.pick_prize(results)
        >>> mega.prize
        2500
        >>> mega.contents = [0, 2]
        >>> mega.pick_prize(results)
        >>> mega.prize
        0
        >>> mega = MegaWinningsTicket(10)
        >>> results = MegaWinningsTicket(10)
        >>> mega.contents = [1, 2, 3]
        >>> results.contents = [1, 2, 3]
        >>> mega.pick_prize(results)
        >>> mega.prize
        10000
        >>> mega.contents = [1, 2, 4]
        >>> mega.pick_prize(results)
        >>> mega.prize
        5000
        >>> mega.contents = [1, 5, 4]
        >>> mega.pick_prize(results)
        >>> mega.prize
        0
        """
        
        if self.price == 5:
            if self == results:
                self.prize = 2500
            else:
                self.prize = 0
        
        if self.price == 10:
            numbers = list(results.contents)
            for num in self.contents:
                if num in numbers:
                    numbers.remove(num)
            if len(numbers) == 0:
                self.prize = 10000
            if len(numbers) == 1:
                self.prize = 5000
            if len(numbers) >= 2:
                self.prize = 0

Pick 2 was easy to implement since it just relies on using what we wrote on with __eq__. Pick 3 needed to check if at the very least 2 numbers were the same, so we couldn't have simply used the __eq__ approach. Though we could have if the conditions were equally as harsh.
                    
    def is_mega_winner(self):
        """ (MegaWinningsTicket) -> str
        
        Return a string indicating if MegaWinningsTicket is a winner 
        
        >>> mega = MegaWinningsTicket(5)
        >>> mega.is_mega_winner()
        'Sorry, try again'
        >>> mega.prize = 1000
        >>> mega.is_mega_winner()
        'Sorry, try again'
        >>> mega.prize = 2500
        >>> mega.is_mega_winner()
        'MEGA WINNER!'
        >>> mega = MegaWinningsTicket(10)
        >>> mega.is_mega_winner()
        'Sorry, try again'
        >>> mega.prize = 10000
        >>> mega.is_mega_winner()
        'MEGA WINNER!'
        >>> mega = MegaWinningsTicket(15)
        >>> mega.is_mega_winner()
        'Sorry, try again'
        >>> mega.prize = 50000
        >>> mega.is_mega_winner()
        'MEGA WINNER!'
        >>> mega = MegaWinningsTicket(20)
        >>> mega.is_mega_winner()
        'Sorry, try again'
        >>> mega.prize = 100000
        >>> mega.is_mega_winner()
        'MEGA WINNER!'
        >>> mega.prize = 10000000
        >>> mega.is_mega_winner()
        'ULTIMATE MEGA WINNER! CONGRATULATIONS'
        """
        
        if self.prize == 10000000:
            return 'ULTIMATE MEGA WINNER! CONGRATULATIONS'
        elif 10000000 > self.prize >= 2500:
            return 'MEGA WINNER!'
        else:
            return 'Sorry, try again'

Last and not least is a simple method that tells us whether or not the prize we have is worth of a mega win. A mega win is dictated by if we get a prize who is larger than 2500. Though it we were to get the Golden Diamond in Golden Diamond, we would receive a more congratulatory string. 

There's so many other subclasses of LotteryTicket we can create, each having their own implementations of the methods necessary for the class, some which may overwrite the code, others which may extend it. Though one thing is for sure...trying to win these games might not be as simple. 

No comments:

Post a Comment