Python Object-Oriented Python Dice Roller RPG Roller

Leo Marco Corpuz
Leo Marco Corpuz
18,755 Points

Dice roll challenge part 2

I’m not sure if my code even makes sense. I’ve checked to see if anyone else was stuck on this challenge and I’ve read that the Hand.roll(2) is a class method. For the init function, are we just assigning two D20 class dice? I’m not sure where the randint() function comes in for this challenge.

dice.py
import random


class Die:
    def __init__(self, sides=2):
        if sides < 2:
            raise ValueError("Can't have fewer than two sides")
        self.sides = sides
        self.value = random.randint(1, sides)

    def __int__(self):
        return self.value

    def __add__(self, other):
        return int(self) + other

    def __radd__(self, other):
        return self + other

class D20(Die):
    def __init__(self):
       super().__init__(sides=20)
hands.py
import random

class Hand(list):
    def __init__(self, size=None, die_class):
        for _ in range(size):
            self.append(die_class)
        self.sort()    



    @property
    def total(self):
        return sum(self)
    @classmethod
    def roll(cls):
        return cls(size=2,die_class=D20)

2 Answers

Louise St. Germain
MOD
Louise St. Germain
Treehouse Moderator 15,568 Points

Hi Leo,

You're definitely not the only one to have struggled with this challenge, so I think it's normal. Kudos to you for sticking with it - you'll figure it out!

There are a few small problems with your code that are preventing it from working the way you want, so I'll go through those and hopefully you can move forward from there. Here's your original code. I added comments with numbers so it will be easier to reference the lines to talk about below.

import random # 1

class Hand(list):
    def __init__(self, size=None, die_class): # 2
# 3
        for _ in range(size):
            self.append(die_class) # 4
        self.sort() # 5   



    @property
    def total(self):
        return sum(self)
    @classmethod
    def roll(cls): # 6
        return cls(size=2,die_class=D20) # 7

# 1 - Import

You mentioned that you're not sure where randint() fits in to this challenge, and basically, you don't need to worry about it. It's already there in dice.py to pick a random # from 1 to 6 when "rolling" the dice. It otherwise isn't used in this program. So in Hands.py, you don't need to import random.

However, you do need to import that D20 class you created in the other file. If you don't connect dice.py to Hands.py somehow, Hands.py has no clue that dice.py is a related file, so it will create an error if you try to reference D20 without importing it. So remove the import random line and replace it with:

from dice import D20

That way, you are importing the class you're going to need later, and Python will know where to find it. ("dice" is the name of the file containing your D20 definition, without the ".py" extension.)

# 2 - initializing Hand

This is pretty close to the right code. However, die_class will need a default value here, like you did with size right before it. Since the challenge explicitly says it should be D20 die, and that's the class you imported, you might as well default it to D20!

#3 - Initialize a List

In this challenge, the Hand class extends the list built-in type. Or in other words, a Hand is a special kind of list: we start with a plain list and give it some extras (in our case, a few rolled D20s). So the first thing we need to do inside our Hand initialization method is to create the list that we're going to add stuff to! If we don't have this empty list, we can create dice, but they will just be floating around - there's no list to append them to.

To create the list, this is where super() comes in. super() refers to the parent class (in this case, list), and we can use its own built-in initialization function:

# Make an empty list
super().__init__()

#4 - Making a D20 die

The idea of this line is to create a die of type die_class, and append it to the (now initialized) list. You definitely have the right idea! However, to actually make the die, you need to call its init function through ().

        for _ in range(size):
            # Add () after die_class to actually make something happen
            self.append(die_class())

This works because die_class contains a reference to a class, so when you run the code, Python will realize, hey, die_class is set to D20, so die_class() means D20()... OK, let's create a new D20 instance! Without the parentheses, Python will know you have a class reference but will not know it's supposed to do anything with it.

# 5 - self.sort()

This isn't needed, and won't work as written anyway - your list contains D20 objects, not plain numbers, and Python won't know how you want them sorted. So you can just delete this line!

#6 - roll class method definition

This needs an extra parameter, which Kenneth hints at in the challenge question:

I'm going to use code similar to Hand.roll(2) [...]

This can be confusing: a class method will have one more parameter than what the user is calling it with. When you set up a class method, it's always a given that it will have cls as a parameter, which gets the class reference that comes immediately before the method name. (In this case, Hand). Here's a quick illustration:

# User can call class method with no parameters:
Hand.roll() 
# But the corresponding class method definition already has one parameter: cls
@classmethod
def roll(cls):
    # cls refers to Hand, which is from before the "." in the method call

# User can call class method with 1 param:
Hand.roll(3)
# Corresponding class method definition needs 2 parameters: cls + one more
def roll(cls, count):
    # cls is the class Hand
    # count contains that 1st user param: 3

# If user calls with 2 params:
Hand.roll(3, 12)
# Corresponding class method definition needs 3 parameters: cls + 2 more
def roll(cls, count, something_else):
    # cls contains Hand class
    # count contains 3
    # something_else contains 12

# etc!

Kenneth says he wants to call Hand.roll with one parameter (e.g., Hand.roll(2)), so it means that the roll class method actually has 2 parameters:

  1. cls (as always)
  2. another one to contain how many dice should be in the hand.
@classmethod
def roll(cls, size=2):
# Can initialize it to whatever... I just made it 2 to match his example.
# It will get replaced when the user specifies the # of dice they want.

# 7 - returning the class instance

(Almost done! :-)) Now that we have the number of dice the user wants, we can pass that to the init method, instead of hard-coding 2 dice. Also, if you made D20 the default in the init method(on the line labeled #2), you don't need to pass it as a parameter here. So you can make this change:

    @classmethod
    def roll(cls, size=2):
        # size will contain the hand size the user specifies. If not 
        # specified, defaults to 2. Can remove die_class param
        # if you set the default to D20 up in the init method.
        return cls(size=size)

I know this is a lot, but I hope it was helpful!

Leo Marco Corpuz
Leo Marco Corpuz
18,755 Points

Thanks for your help! However, I don’t understand the last part (class method) where you just set size=size instead of 2. When I used “return cls(size=2)” I got an error saying I got the wrong length for the Hand class. Does it mean that for class methods, parameters can be changed when class methods are called? If that’s true, I think it makes sense so when you have multiple set parameters you would be able to change at least one of them.

Louise St. Germain
Louise St. Germain
Treehouse Moderator 15,568 Points

Yes, the user can call your roll() class method with whatever number they want. It's going to look like this when they call the function:

myHand = Hand.roll(4)  # Or whatever number they want

Also, size=size might be confusing, so to illustrate what's going on a bit better, I will also change the parameter name for the hand size in the roll class method (you can skip this part if it's already clear!). return cls(size=size) in the original code refers to two different "size" variables/parameters, so I'll differentiate them here.

    @classmethod
    def roll(cls, user_defined_size=2):
        # The first mention of size refers to the parameter named "size" in 
        # our __init__ method, not our local hand size parameter.
        return cls(size=user_defined_size)
        # So we are telling Python, run the __init__ method, but instead of size=None,
        # make size = what the user gave to us here in the roll method.

When you do:

return cls(size=2)

I think in the background the challenge is checking with more than one hand size (i.e., numbers other than 2) to see if it responds. The above code will always generate a hand with 2 die even if the user says they want 3 or 19, so that might've been the issue.

Anyway, sounds like things are either working now, or getting close, which is great! (I think anyone who can get through an answer as long as what I had probably deserves a badge just for that! :-))