# Lab 6: Object-Oriented Programming lab06.zip

Due at 11:59pm on Friday, 03/02/2018.

## Starter Files

Download lab06.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. Check that you have successfully submitted your code on okpy.org.

• Questions 1, 2, and 3, must be completed in order to receive credit for this lab. Questions 2 is done in the `data.py` file and Question 3 is done in `classes.py`.
• Question 4 is optional. It is recommended that you complete this problem if you finish the required portion early. The starter code for this problem is in `lab06_extra.py`.
• Questions 5 through 8 are optional. It is recommended that you complete these problems on your own time. The starter code for these problems is in `classes.py`.

# 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.

## Nonlocal

We say that a variable defined in a frame is local to that frame. A variable is nonlocal to a frame if it is defined in the environment that the frame belongs to but not the frame itself, i.e. in its parent or ancestor frame.

So far, we know that we can access variables in parent frames:

``````def make_adder(x):
""" Returns a one-argument function that returns the result of
adding x and its argument. """
return x + y

Here, when we call `make_adder`, we create a function `adder` that is able to look up the name `x` in `make_adder`'s frame and use its value.

However, we haven't been able to modify variable in parent frames. 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``````

The inner function `counter` attempts to update the vairable `count` in its parent frame. 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!

Some important things to keep in mind when using `nonlocal`

• `nonlocal` cannot be used with global variables (names defined in the global frame).
• If no nonlocal variable is found with the given name, a `SyntaxError` is raised.
• A name that is already local to a frame cannot be declared as nonlocal.

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.

## Object-Oriented Programming

In this lab we'll be diving 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.

## OOP Example: Car Class

Professor Hilfinger 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 -- for now...

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.

### Constructor

Let's build Professor Hilfinger 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 occurrence, 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 the body of the constructor for now.

Let's make our car. Professor Hilfinger 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.

``>>> hilfingers_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, ``` hilfingers_car``` is now bound to a `Car` object or, in other words, an instance of the `Car` class.

### Attributes

So how are the `make` and `model` of Professor Hilfinger'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
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 `hilfingers_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.

You might notice in the `__init__` method of the `Car` class, the instance attribute `gas` is initialized to the value of `Car.gas`, the class attribute. Why don't we just use the class attribute, then? The reason is because each `Car`'s `gas` attribute needs to be able to change independently of each other. If one `Car` drives for a while, it should use up some `gas`, and that `Car` instance should reflect that by having a lower `gas` value. However, all other `Car`s shouldn't lose any `gas`, and changes to a class attribute will affect all instances of the class.

#### Dot Notation

Class attributes can also be accessed using dot notation, both on an instance and on the class name itself. For example, we can access the class attribute `size` of `Car` like this:

``````>>> Car.size
'Tiny'``````

And in the following line, we access `hilfingers_car`'s `color` attribute:

``````>>> hilfingers_car.color
'No color yet. You need to paint me.'``````

Looks like we need to paint `hilfingers_car`!

### Methods

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!

``````>>> hilfingers_car.paint('black')
'Tesla Model S is now black'
>>> hilfingers_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, `hilfingers_car` is bound to `self` so that the body of `paint` can access its attributes!

You can also call methods using the class name and dot notation; for example,

``````>>> Car.paint(hilfingers_car, 'red')
'Tesla Model S is now red'``````

Notice that unlike when we painted Professor Hilfinger's car black, this time we had to pass in two arguments: one for `self` and one for `color`. This is because when you call a method using dot notation from an instance, Python knows what instance to automatically bind to `self`. However, when you call a method using dot notation from the class, Python doesn't know which instance of `Car` we want to paint, so we have to pass that in as well.

### Inheritance

Professor Hilfinger's red 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 Professor Hilfinger's monster truck:

``>>> hilfingers_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.

``````>>> hilfingers_car.size
'Tiny'
>>> hilfingers_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 `Car`s cannot `rev`! Everything else -- the constructor `__init__`, `paint`, `num_wheels`, `gas` -- are inherited from `Car`.

# Required Questions

## WWPD

### Q1: Using the Car class

Here are the full definitions of the `Car` and `MonsterTruck` classes from the car example above in `car.py`:

``````class Car(object):
num_wheels = 4
gas = 30
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 += 20
return self.make + ' ' + self.model + ' gas level: ' + str(self.gas)

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 car -u
python3 ok -q food_truck -u``````

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

``````>>> hilfingers_car = Car('Tesla', 'Model S')
>>> hilfingers_car.color
______'No color yet. You need to paint me.'
>>> hilfingers_car.paint('black')
______'Tesla Model S is now black'
>>> hilfingers_car.color
______'black'``````
``````>>> hilfingers_car = Car('Tesla', 'Model S')
>>> hilfingers_truck = MonsterTruck('Monster Truck', 'XXL')
>>> hilfingers_car.size
______'Tiny'
>>> hilfingers_truck.size
______'Monster'``````
``````>>> hilfingers_car = Car('Tesla', 'Model S')
>>> hilfingers_car.model
______'Model S'
>>> hilfingers_car.gas = 10
>>> hilfingers_car.drive()
______'Tesla Model S goes vroom!'
>>> hilfingers_car.drive()
______'Tesla Model S cannot drive!'
>>> hilfingers_car.fill_gas()
______'Tesla Model S gas level: 20'
>>> hilfingers_car.gas
______20
>>> Car.gas
______30``````
``````>>> Car.headlights
______2
______2
______3
______3``````
``````>>> hilfingers_car.wheels = 2
>>> hilfingers_car.wheels
______2
>>> Car.num_wheels
______4
>>> hilfingers_car.drive()
______'Tesla Model S cannot drive!'
>>> Car.drive()
______Error (TypeError)
>>> Car.drive(hilfingers_car)
______'Tesla Model S cannot drive!'
>>> MonsterTruck.drive(hilfingers_car)
______Error (AttributeError)``````
``````>>> deneros_car = MonsterTruck('Monster', 'Batmobile')
>>> deneros_car.drive()
______Vroom! This Monster Truck is huge!
'Monster Batmobile goes vroom!'
>>> Car.drive(deneros_car)
______'Monster Batmobile goes vroom!'
>>> MonsterTruck.drive(deneros_car)
______Vroom! This Monster Truck is huge!
'Monster Batmobile goes vroom!'
>>> Car.rev(deneros_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!'``````

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``

To exit the game and return to the command line, type `Ctrl-C` or `Ctrl-D`.

### Q2: 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('Squirrel McSquirrelFace', sather_gate)``````

Use Ok to test your code:

``python3 ok -q me``

``python3 adventure.py``

You will see the following output (though the commands may be in a different order):

``````Welcome to the adventure game!

It's a bright sunny day.
You are a bright and eager CS 61A student named [your name],
wandering around Berkeley campus looking for some snacks
before the study party.

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

All you're able to do right now is look around, but let's change that!

### Q3: Where do I go?

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.

``````    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', 'Sather Gate', [], [])
>>> gbc = Place('GBC', 'Golden Bear Cafe', [], [])
>>> sather_gate.locked = True
>>> 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')
else:
self.place = destination_place
print('You are at', self.place.name)``````

Use Ok to test your code:

``python3 ok -q Player.go_to``

After you complete this problem, you'll be able to move to different places and look around there. To complete the functionality of the game, including talking to characters and picking up items, finish Questions 5-8 under the Optional Questions section!

# Optional Questions

## Nonlocal Practice

### Q4: 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 sequence of snacks, returning one element from the sequence in order.

``````def vending_machine(snacks):
"""Cycles through sequence of snacks.

>>> vender = vending_machine(('chips', 'chocolate', 'popcorn'))
>>> vender()
'chips'
>>> vender()
'chocolate'
>>> vender()
'popcorn'
>>> vender()
'chips'
>>> other = vending_machine(('brownie',))
>>> other()
'brownie'
>>> vender()
'chocolate'
"""
index = 0
def vender():
nonlocal index
result = snacks[index]
index = (index + 1) % len(snacks)
return result
return vender``````

Use Ok to test your code:

``python3 ok -q vending_machine``

### Q5: How do I talk?

Now you can go wherever you want! Try going to Wheeler. There, you'll find Jerry. 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.

>>> jerry = Character('Jerry', 'I am not the Jerry you are looking for.')
>>> wheeler = Place('Wheeler', 'You are at Wheeler', [jerry], [])
>>> me = Player('player', wheeler)
>>> me.talk_to(jerry)
Person has to be a string.
>>> me.talk_to('Jerry')
Jerry says: I am not the Jerry you are looking for.
>>> me.talk_to('Tiffany')
Tiffany is not here.
"""
if type(person) != str:
print('Person has to be a string.')
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``

### Q6: 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

>>> lemon = Thing('Lemon', 'A lemon-looking lemon')
>>> gbc = Place('GBC', 'You are at Golden Bear Cafe', [], [lemon])
>>> me = Player('Player', gbc)
>>> me.backpack
[]
>>> me.take(lemon)
Thing should be a string.
>>> me.take('orange')
orange is not here.
>>> me.take('Lemon')
Player takes the Lemon
>>> me.take('Lemon')
Lemon is not here.
>>> isinstance(me.backpack[0], Thing)
True
>>> len(me.backpack)
1
"""
if type(thing) != str:
print('Thing should be a string.')
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``

### Q7: 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:

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', [], [])
>>> 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')
>>> 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
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``

### Q8: 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?

``python3 adventure.py``

Enjoy!