Container Encapsulation and Inheritance Lab

Starter Code

Steps

  1. Check out the line my_box = Box(50, 50, 4)  # x, y, max_capacity. What should the __init__ method look like? Code it in box.py.

  2. Add three default attributes for the Box class. contents: List[int] = [], width: int = 75, and height: int = 50.

  3. Check out the line my_box.draw(screen). Define a draw() method for this in box.py. Don’t forget self and the ability to take a screen object as an argument.

  4. Check out the line if my_box.is_clicked(mouse_location). Create the method definition for this in box.py.

By this point, when you click the box, new contents should be added to it.

  1. What happens when you click more than four times? What is the capacity? What is wrong with this? Identify the line of code that allowed this bug to happen. How can this be fixed? There are two possible solutions. The best solution envolves proper encapsulation of the contents list.

  2. Create an add_item method in the Box class. It should be defined to take a single integer (which we willl deem as a single item). This method will add the integer to the contents list, but only if the box isn’t already full.

  3. Change the un-encapsulated my_box.contents.append(1) in main.py to make use of the handy new add_item method.

By this point, items should be added to the box if it isn’t full. So, the display text on the box shouldn’t go passed 4/4.

Note

Optional (Advanced)

If you wanted to display some message saying something to effect of “you can’t add anything else (because it’s full), you could have the Box object raise an error.

  • In box.py create a new class called BoxFullError.

  • Have this error class inherit from Exception.

  • In your add_item method, if you try to add an item and the container is full, raise this BoxFullError.

Now if you try to add an item to a full box, the program will crash. This is fine, because we can have main.py catch that error.

  • Where main.py adds a new item, wrap that line of code in a try/except.

  • When the BoxFullError happens, you can simply print out to the console "Box is full". In reality, you could display some text near the box in the game or play a sound.

Why go through all this trouble when you can just print out the message in the Box class add_item method? The answer is in the form of a question: do you want every single person using your Box class to be forced to print the error message "Box is full" to the console, or do you want to give each programmer the opportunity to handle that particular error in whatever way that best fits their particular situation?

Let’s say we wanted the ability for some boxes to be lockable. Normal boxes would not have that capability, but those with locks should have the exact capabilities as a normal Box.

  1. Create a new class (in box.py) called LockableBox and inherit from the Box class. It should have all the functionality of a regular box, but with added functionality (don’t worry about this yet). This is achieved through inheritance.

  2. In main.py create a LockableBox along-side the other box. You will need to call the draw() method, as well as handle mouse clicks, just like the regular box. Confirm that the box functions exactly the same as the regular box. hint: make sure you import ``LockableBox`` from ``box``, just like the normal box. I forgot. Oops.

  3. It might be best to store these two boxes in a list now, so you can loop through them when checking for clicks and drawing them. Do this and ensure both boxes are functioning properly.

Now we add the extra functionality specific to LockableBox. We want to:

  1. Override the __init__ method in LockableBox and a locked attribute. Make this private and default to True (just because). The method definition should be exactly the same as the original Box __init__ method. If you wanted to, you could add a locked parameter to it.

  2. If you have not copy-and-pasted the original method, chances are your game doesn’t run and you get some AttributeError saying LockableBox has no attribute 'x'. Make sure to copy-paste the original method that initializes self.x, self.y, self.contents etc.

Verify that the boxes work. You might notice that even though we have a locked attribute, that doesn’t prevent us from adding items. We will get to that in a minute.

For LockableBox.__init__, we need to make sure we don’t copy and paste code from the parent class (Box). That would defeat the advantage of inheritance. If we simply get rid of that code we get an error. So how do we get the original Box.__init__ method to run as well? We need to use super().__init__(). This will manually call the parent-class’s __init__ method that will take care of the attributes x, y, contents, and max_capacity.

  1. In LockableBox.__init__, before you set the locked attribute, make a call to super().__init__(). Be sure to give that init method all the information it needs. Hint: ``Box(x, y, max_capacity)``. Verify that both boxes work.

  2. Lockable boxes now need to reject items if the boxes are locked. You need to override the add_item method. You can simply copy and paste the method from Box and add the code you need, but why not make use of super() to call the original method? optional, advanced: create a BoxIsLockedError that you can raise if they try to add an item when the box is locked. If you do this, don’t forget to import it into main.py and catch the specific error when you call add_item.

Let’s display on the box if it is locked.

  1. Override the draw() method, call super(). Don’t forget to pass along the screen argument. Then use the lines below to display "locked" on the box:

text = FONT.render("Locked", True, (0, 0, 0))
screen.blit(text, (self.x, self.y + self.height//2))
  1. Let’s add a way to unlock the box. Becasue I instructed to make the locked attribute private, create a method called toggle_lock. This will lock when it is unlocked, and unlock when it is locked. The cool thing is our main.py code doesn’t have to worry about how this happens, all you have to do is call lockable_box.toggle_lock() and the box itself will take care of it. You can also make more realistic lock and unlock methods that can take a key as an argument, and depending on if the key matches, it will unlock or not.

  2. In main.py we need to modify the event section under MOUSEBUTTONDOWN. We want to be able to distinguish between a mouse right-click and a left click. Left-click will add items and right-click will toggle the lock. Because this is Pygame specific, I’ll give you the code to replace the MOUSEBUTTONDOWN section of the event loop.

    elif event.type == MOUSEBUTTONDOWN:
        mouse_location = event.pos
        for box in boxes:
            if box.is_clicked(mouse_location):
                if event.button == 1:  # left-click
                    try:
                        box.add_item(1)
                    except BoxFullError:
                        print("The box is full!")
                    except BoxIsLockedError:
                        print("The box is locked!")
                elif event.button == 3:  # right-click
                    # TOGGLE THE LOCK
    

    Confirm this works on the lockable box. Note: if you call .toggle_lock() on the regular box, you will get an AttributeError. You can use try/except to handle it, or use if isinstance(box, LockableBox): to only call the method on the appropriate boxes.