Due at 11:59pm on 07/19/2016.

Starter Files

Download lab08.zip. Inside the archive, you will find starter files for the questions in this lab, along with a copy of the OK autograder.

Submission

By the end of this lab, you should have submitted the lab with python3 ok --submit. You may submit more than once before the deadline; only the final submission will be graded.

  • Questions 1, 2, and 3 must be completed in order to receive credit for this lab.
  • Questions 4 through 8 are optional. It is recommended that you complete these problems on your own time.

Helpful Hints

OK has a new feature for the "What would Python display?" questions. As you go through the question's prompts OK may give you a hint based on your wrong answers. Our hope is these hints will help remind you of something important or push you in the right direction to getting the correct answer. For example:

Foo > Suite 1 > Case 1
(cases remaining: 1)

What would Python display? If you get stuck, try it out in the Python
interpreter!

>>> not False or False
? False

-- Helpful Hint --
What about the | not |?
------------------

-- Not quite. Try again! --

? 

Topics

Consult this section if you need a refresher on the material for this lab. It's okay to skip directly to the questions and refer back here should you get stuck.

Object-Oriented Programming

Let's dive into object-oriented programming (OOP), a model of programming that allows you to think of data in terms of "objects" with their own characteristics and actions, just like objects in real life! This is very powerful and allows you to create objects that are specific to your program — you can read up on all the details here. For now, we'll guide you through it as you build your very own text-based adventure game!

OOP and Inheritance Example: Cars

Brian is running late, and needs to get from San Francisco to Berkeley before lecture starts. He'd take BART, but that will take too long. It'd be great if he had a car. A monster truck would be best, but a car will do.

In car.py, you'll find a class called Car. A class is a blueprint for creating objects of that type. In this case, the Car class statement tells us how to create Car objects.

So let's build Brian a car! Don't worry, you won't need to do any physical work — the constructor will do it for you. The constructor of a class is a function that creates an instance, or one single occurence, of the object outlined by the class. In Python, the constructor method is named __init__. Note that there must be two underscores on each side of init. The Car class' constructor looks like this:

def __init__(self, make, model):
    self.make = make
    self.model = model
    self.color = 'No color yet. You need to paint me.'
    self.wheels = Car.num_wheels
    self.gas = Car.gas

The __init__ method for Car has three parameters. The first one, self, is automatically bound to the newly created Car object. The second and third parameters, make and model, are bound to the arguments passed to the constructor, meaning when we make a Car object, we must provide two arguments. Don't worry about the code inside of the constructor for now.

Let's make our car. Brian would like to drive a Tesla Model S to lecture. We can construct an instance of Car with 'Tesla' as the make and 'Model S' as the model. Rather than calling __init__ explicitly, Python allows us to make an instance of a class by using the name of the class.

>>> brians_car = Car('Tesla', 'Model S')

Here, 'Tesla' is passed in as the make, and 'Model S' as the model. Note that we don't pass in an argument for self, since its value is always the object being created. An object is an instance of a class. In this case, brians_car is now bound to a Car object or, in other words, an instance of the Car class.

So how are the make and model of Brian's car actually stored? Let's talk about attributes of instances and classes. Here's a snippet of the code in car.py of the instance and class attributes in the Car class:

class Car(object):
    num_wheels = 4
    gas = 30
    headlights = 2
    size = 'Tiny'

    def __init__(self, make, model):
        self.make = make
        self.model = model
        self.color = 'No color yet. You need to paint me.'
        self.wheels = Car.num_wheels
        self.gas = Car.gas

    def paint(self, color):
        self.color = color
        return self.make + ' ' + self.model + ' is now ' + color

In the first two lines of the constructor, the name self.make is bound to the first argument passed to the constructor and self.model is bound to the second. These are two examples of instance attributes. An instance attribute is a quality that is specific to an instance, and thus can only be accessed using dot notation (separating the instance and attribute with a period) on an instance. In this case, self is bound to our instance, so self.model references our instance's model.

Our car has other instance attributes too, like color and wheels. As instance attributes, the make, model, and color of brians_car do not affect the make, model, and color of other cars.

On the other hand, a class attribute is a quality that is shared among all instances of the class. For example, the Car class has four class attributes defined at the beginning of a class: num_wheels = 4, gas = 30, headlights = 2 and size = 'Tiny'. The first says that all cars have 4 wheels. Class attributes can also be accessed using dot notation, both on an instance and on the class name itself.

In the following line, we access brians_car's color attribute:

>>> brians_car.color
'No color yet. You need to paint me.'

Looks like we need to paint brians_car!

Let's use the paint method from the Car class. Methods are functions that are specific to a class; only an instance of the class can use them. We've already seen one method: __init__! Think of methods as actions or abilities of objects. How do we call methods on an instance? You guessed it, dot notation!

>>> brians_car.paint('black')
'Tesla Model S is now black'
>>> brians_car.color
'black'

Awesome! But if you take a look at the paint method, it takes two parameters. So why don't we need to pass two arguments? Just like we've seen with __init__, all methods of a class have a self parameter to which Python automatically binds the instance that is calling that method. Here, brians_car is bound to self so that the body of paint can access its attributes!

Brian's black Tesla is pretty cool, but it has to sit in traffic. How about we create a monster truck for him instead? In car.py, we've defined a MonsterTruck class. Let's look at the code for MonsterTruck:

class MonsterTruck(Car):
    size = 'Monster'

    def rev(self):
        print('Vroom! This Monster Truck is huge!')

    def drive(self):
        self.rev()
        return Car.drive(self)

Wow! The truck may be big, but the source code is tiny! Let's make sure that the truck still does what we expect it to do. Let's create a new instance of Brian's monster truck:

>>> brians_truck = MonsterTruck('Monster Truck', 'XXL')

Does it behave as you would expect a Car to? Can you still paint it? Is it even drivable?

Well, the class MonsterTruck is defined as class MonsterTruck(Car):, meaning its superclass is Car. Likewise, the class MonsterTruck is a subclass of the Car class. That means the MonsterTruck class inherits all the attributes and methods that were defined in Car, including its constructor!

Inheritance makes setting up a hierarchy of classes easier because the amount of code you need to write to define a new class of objects is reduced. You only need to add (or override) new attributes or methods that you want to be unique from those in the superclass.

>>> brians_car.size
'Tiny'
>>> brians_truck.size
'Monster'

Wow, what a difference in size! This is because the class attribute size of MonsterTruck overrides the size class attribute of Car, so all MonsterTruck instances are 'Monster'-sized.

In addition, the drive method in MonsterTruck overrides the one in Car. To show off all MonsterTruck instances, we defined a rev method specific to MonsterClass. Regular Cars cannot rev! Everything else — the constructor __init__, paint, num_wheels, gas — are inherited from Car.

Nonlocal

Consider the following function:

def make_counter():
    """Makes a counter function.

    >>> counter = make_counter()
    >>> counter()
    1
    >>> counter()
    2
    """
    count = 0
    def counter():
        count = count + 1
        return count
    return counter

Running this function's doctests, we find that it causes the following error:

UnboundLocalError: local variable 'count' referenced before assignment

Why does this happen? When we execute an assignment statement, remember that we are either creating a new binding in our current frame or we are updating an old one in the current frame. For example, the line count = ... in counter, is creating the local variable count inside counter's frame. This assignment statement tells Python to expect a variable called count inside counter's frame, so Python will not look in parent frames for this variable. However, notice that we tried to compute count + 1 before the local variable was created! That's why we get the UnboundLocalError.

To avoid this problem, we introduce the nonlocal keyword. It allows us to update a variable in a parent frame! Note we cannot use nonlocal to modify variables in the global frame. Consider this improved example:

 def make_counter():
    """Makes a counter function.

    >>> counter = make_counter()
    >>> counter()
    1
    >>> counter()
    2
    """
    count = 0
    def counter():
        nonlocal count
        count = count + 1
        return count
    return counter

The line nonlocal count tells Python that count will not be local to this frame, so it will look for it in parent frames. Now we can update count without running into problems.

Required Questions

Question 1: WWPD: Using the Car class

Here is the code from the car example in car.py:

class Car(object):
    num_wheels = 4
    gas = 30
    headlights = 2
    size = 'Tiny'

    def __init__(self, make, model):
        self.make = make
        self.model = model
        self.color = 'No color yet. You need to paint me.'
        self.wheels = Car.num_wheels
        self.gas = Car.gas

    def paint(self, color):
        self.color = color
        return self.make + ' ' + self.model + ' is now ' + color

    def drive(self):
        if self.wheels < Car.num_wheels or self.gas <= 0:
            return self.make + ' ' + self.model + ' cannot drive!'
        self.gas -= 10
        return self.make + ' ' + self.model + ' goes vroom!'

    def pop_tire(self):
        if self.wheels > 0:
            self.wheels -= 1

    def fill_gas(self):
        self.gas += 30
        print('Your car is full.')

class MonsterTruck(Car):
    size = 'Monster'

    def rev(self):
        print('Vroom! This Monster Truck is huge!')

    def drive(self):
        self.rev()
        return Car.drive(self)

Use OK to test your knowledge with the following What would Python Display questions.

python3 ok -q prologue -u
python3 ok -q car -u

If an error occurs, type Error. If nothing is displayed, type Nothing.

>>> brians_car = Car('Tesla', 'Model S')
>>> brians_car.color
______
'No color yet. You need to paint me.'
>>> brians_car.paint('black')
______
'Tesla Model S is now black'
>>> brians_car.color
______
'black'
>>> brians_car = Car('Tesla', 'Model S')
>>> brians_truck = MonsterTruck('Monster Truck', 'XXL')
>>> brians_car.size
______
'Tiny'
>>> brians_truck.size
______
'Monster'
>>> brians_car = Car('Tesla', 'Model S')
>>> brians_car.model
______
'Tesla'
>>> brians_car.gas = 10 >>> brians_car.drive()
______
'Tesla Model S goes vroom!'
>>> brians_car.drive()
______
'Tesla Model S cannot drive!'
>>> brians_car.fill_gas()
______
Your car is full.
>>> brians_car.gas
______
30
>>> Car.headlights
______
2
>>> brians_car.headlights
______
2
>>> Car.headlights = 3 >>> brians_car.headlights
______
3
>>> brians_car.headlights = 2 >>> Car.headlights
______
3
>>> brians_car.wheels = 2
>>> brians_car.wheels
______
2
>>> Car.num_wheels
______
4
>>> brians_car.drive()
______
'Tesla Model S cannot drive!'
>>> Car.drive()
______
Error (TypeError)
>>> Car.drive(brians_car)
______
'Tesla Model S cannot drive!'
>>> MonsterTruck.drive(brians_car)
______
Error (AttributeError)
>>> marvins_car = MonsterTruck('Monster', 'Batmobile')
>>> marvins_car.drive()
______
Vroom! This Monster Truck is huge! 'Monster Batmobile goes vroom!'
>>> Car.drive(marvins_car)
______
'Monster Batmobile goes vroom!'
>>> MonsterTruck.drive(marvins_car)
______
Vroom! This Monster Truck is huge! 'Monster Batmobile goes vroom!'
>>> Car.rev(marvins_car)
______
Error (AttributeError)
>>> class FoodTruck(MonsterTruck):
...    delicious = 'meh'
...    def serve(self):
...        if FoodTruck.size == 'delicious':
...            print('Yum!')
...        if self.food != 'Tacos':
...            return 'But no tacos...'
...        else:
...            return 'Mmm!'
>>> taco_truck = FoodTruck('Tacos', 'Truck')
>>> taco_truck.food = 'Guacamole'
>>> taco_truck.serve()
______
'But no tacos...'
>>> taco_truck.food = taco_truck.make >>> FoodTruck.size = taco_truck.delicious >>> taco_truck.serve()
______
'Mmm!'
>>> taco_truck.size = 'delicious' >>> taco_truck.serve()
______
'Mmm!'
>>> FoodTruck.pop_tire()
______
Error
>>> FoodTruck.pop_tire(taco_truck)
______
Nothing
>>> taco_truck.drive()
______
Vroom! This Monster Truck is huge! 'Tacos Truck cannot drive!'

Adventure Game!

In the next part of this lab, we will be implementing a text-based adventure game! You can start the game by typing:

python3 adventure.py

Question 2: Who am I?

It is time for you to enter a world of adventure! First, you need to create yourself as a Player object in data.py. Take a look at the Player class in classes.py and create a Player object at the bottom of data.py.

The Player constructor takes two arguments:

  • name should be your preferred name (as a string)
  • the starting place

Your Player should start at sather_gate.

# Player:
# The Player should start at sather_gate.
me = None
me = Player('Your Name', sather_gate)

Use OK to test your code:

python3 ok -q me

Question 3: Where do I go?

Once you've created your player, you can start the adventure game:

python3 adventure.py

You will see the following output:

Welcome to the adventure game!

It's a bright sunny day.
You are a cute little squirrel named [your name],
wandering around Berkeley campus looking for food.

Let's go to FSM (Free Speech Movement Cafe)
and see what we can find there!

There are 7 possible commands:
    talk to [character]
    unlock [place]
    help
    take [thing]
    go to [place]
    look
    check backpack

adventure>

First, we need to be able to go to places. If you try the go to command, you'll notice it doesn't do anything.

In classes.py, complete the go_to method in the Player class by updating your place attribute to destination_place if it is not locked, and printing the name of the current place. Note the place's description may not always include the name of the place.

def go_to(self, location):
    """Go to a location if it's among the exits of player's current place.

    >>> sather_gate = Place('Sather Gate', 'You are at Sather Gate', [], [])
    >>> gbc = Place('GBC', 'You are at Golden Bear Cafe', [], [])
    >>> sather_gate.add_exits([gbc])
    >>> sather_gate.locked = True
    >>> gbc.add_exits([sather_gate])
    >>> me = Player('player', sather_gate)
    >>> me.go_to('GBC')
    You are at GBC
    >>> me.place is gbc
    True
    >>> me.place.name
    'GBC'
    >>> me.go_to('GBC')
    Can't go to GBC from GBC.
    Try looking around to see where to go.
    You are at GBC
    >>> me.go_to('Sather Gate')
    Sather Gate is locked! Go look for a key to unlock it
    You are at GBC
    """
    destination_place = self.place.get_neighbor(location)
    if destination_place.locked:
        print(destination_place.name, 'is locked! Go look for a key to unlock it')
"*** YOUR CODE HERE ***"
else: self.place = destination_place print('You are at', self.place.name)

Use OK to test your code:

python3 ok -q Player.go_to

Optional Questions

More Adventure!

Question 4: How do I talk?

Now you can go wherever you want! Try going to Wheeler Hall. There, you'll find Derrick. Try talking to him with the talk to command. This also doesn't work :(

Next, implement the talk_to method in Player. talk_to takes in the name of a Character object, and prints out the Character's response. Take a look at the doctest for more details.

Hint: talk_to takes in an argument person, which is a string. The characters instance attribute in self.place is a dictionary mapping Character names (strings) to Character objects.

Once you've got the Character object, what method in the Character class will make them talk?

def talk_to(self, person):
    """Talk to person if person is at player's current place.

    >>> john = Character('John', 'Have to run for lecture!')
    >>> sather_gate = Place('Sather Gate', 'You are at Sather Gate', [john], [])
    >>> me = Player('player', sather_gate)
    >>> me.talk_to(john)
    Person has to be a string.
    >>> me.talk_to('John')
    John says: Have to run for lecture!
    >>> me.talk_to('Albert')
    Albert is not here.
    """
    if type(person) != str:
        print('Person has to be a string.')
"*** YOUR CODE HERE ***"
elif person not in self.place.characters: print(person, 'is not here.') else: print(person,'says:', self.place.characters[person].talk())

Use OK to test your code:

python3 ok -q Player.talk_to

Question 5: How do I take items?

Now you can talk to people in adventure world! To make it even better, let's implement the take method in the Player class so you can put things in your backpack. Currently, you don't have a backpack, so let's create an instance variable backpack and initialize it to an empty list.

After you've initialized your empty backpack, implement take, which takes in a name of an object as a name, checks if the Place you are at contains the Thing object corresponding to the input name, and then puts it into your backpack. Take a look at the doctests for more details.

Hint: the things instance attribute in the Place class is a dictionary that maps Thing names (strings) to Thing objects.

The take method in the Place class will also come in handy.

def take(self, thing):
    """Take a thing if thing is at player's current place

    >>> hotdog = Thing('Hotdog', 'A hot looking hotdog')
    >>> gbc = Place('GBC', 'You are at Golden Bear Cafe', [], [hotdog])
    >>> me = Player('Player', gbc)
    >>> me.backpack
    []
    >>> me.take(hotdog)
    Thing should be a string.
    >>> me.take('dog')
    dog is not here.
    >>> me.take('Hotdog')
    Player takes the Hotdog
    >>> me.take('Hotdog')
    Hotdog is not here.
    >>> isinstance(me.backpack[0], Thing)
    True
    >>> len(me.backpack)
    1
    """
    if type(thing) != str:
        print('Thing should be a string.')
"*** YOUR CODE HERE ***"
elif thing not in self.place.things: print(thing, 'is not here.') else: taken = self.place.take(thing) print(self.name, 'takes the', taken.name) self.backpack.append(taken)

Use OK to test your code:

python3 ok -q Player.take

Question 6: No door can hold us back!

FSM is locked! There's no way for us to get in, and you're getting pretty desperate for that sweet, delicious, caffeinated nectar of the gods.

We'll need to do two things in order to get into FSM and get our caffeine fix.

Firstly, define a new class Key, that is a subclass of Thing, but overwrites the use method to unlock the door to FSM.

Hint 1: Refer back to the MonsterTruck example if you need a refresher on how to define a subclass and overwrite methods. Make sure you define Key after you've defined Thing.

Hint 2: Place has a locked instance attribute that you may need to change.

class Thing(object):
    def __init__(self, name, description):
        self.name = name
        self.description = description

    def use(self, place):
        print("You can't use a {0} here".format(self.name))

""" Implement Key here! """
class Key(Thing): def use(self, place): if place.locked: place.locked = False print(place.name, 'is now unlocked!') else: print(place.name, 'is already unlocked!')

You'll also need to finish the implementation of unlock in Player. It takes a string place that you want to unlock, and if you have a key, call the key's use method to unlock the place. If you have no key, then the method should print that the place can't be unlocked without a key.

You'll need to implement both Key and unlock for the doctests to pass.

def unlock(self, place):
    """If player has a key, unlock a locked neighboring place.

    >>> key = Key('SkeletonKey', 'A Key to unlock all doors.')
    >>> gbc = Place('GBC', 'You are at Golden Bear Cafe', [], [key])
    >>> fsm = Place('FSM', 'Home of the nectar of the gods', [], [])
    >>> gbc.add_exits([fsm])
    >>> fsm.locked = True
    >>> me = Player('Player', gbc)
    >>> me.unlock(fsm)
    Place must be a string
    >>> me.go_to('FSM')
    FSM is locked! Go look for a key to unlock it
    You are at GBC
    >>> me.unlock(fsm)
    Place must be a string
    >>> me.unlock('FSM')
    FSM can't be unlocked without a key!
    >>> me.take('SkeletonKey')
    Player takes the SkeletonKey
    >>> me.unlock('FSM')
    FSM is now unlocked!
    >>> me.unlock('FSM')
    FSM is already unlocked!
    >>> me.go_to('FSM')
    You are at FSM
    """
    if type(place) != str:
        print("Place must be a string")
        return
    key = None
    for item in self.backpack:
        if type(item) == Key:
            key = item
"*** YOUR CODE HERE ***"
if not key: print(place, "can't be unlocked without a key!") else: place_to_unlock = self.place.get_neighbor(place) key.use(place_to_unlock)

Use OK to test your code:

python3 ok -q Player.unlock

Question 7: Win the game!

Good job! Now you can explore around campus and try to win the game. Talk to the people at different places in order to get hints. Can you save the day and make it to the 61A project party in time?

Enjoy!

Nonlocal Practice

Question 8: Vending Machine

Implement the function vending_machine, which takes in a sequence of snacks (as strings) and returns a zero-argument function. This zero-argument function will cycle through the list of snacks, returning one element from the list in order.

def vending_machine(snacks):
    """Cycles through list of snacks.

    >>> vender = vending_machine(['chips', 'chocolate', 'popcorn'])
    >>> vender()
    'chips'
    >>> vender()
    'chocolate'
    >>> vender()
    'popcorn'
    >>> vender()
    'chips'
    >>> other = vending_machine(['brownie'])
    >>> other()
    'brownie'
    >>> vender()
    'chocolate'
    """
"*** YOUR CODE HERE ***"
index = 0 def vender(): nonlocal index result = seq[index] index = (index + 1) % len(seq) return result return vender

Use OK to test your code:

python3 ok -q vending_machine