Lab 10: Inheritance, Debugging, and Prep for Project 2
Due at 11:59:59 pm on Friday, 4/17/2020.
Starter Files
Download lab10.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. See this article for more instructions on okpy and submitting assignments.
- Submit the
lab10.py
file took
.
Inheritance & Exceptions
Question 1: Account
There are several things wrong with the following code! Debug the Account
class to satisfy the docstring.
class Account(object):
"""A bank account that allows deposits and withdrawals.
>>> sophia_account = Account('Sophia')
>>> sophia_account.deposit(1000000) # depositing my paycheck for the week
1000000
>>> sophia_account.transactions
[('deposit', 1000000)]
>>> sophia_account.withdraw(100) # buying dinner
999900
>>> sophia_account.transactions
[('deposit', 1000000), ('withdraw', 100)]
"""
interest = 0.02
balance = 1000
def __init__(self, account_holder):
self.balance = 0
self.holder = account_holder
self.transactions = []
def deposit(self, amount):
"""Increase the account balance by amount and return the
new balance.
"""
self.transactions.append(('deposit', amount))
Account.balance = self.balance + amount
return self.balance
def withdraw(self, amount):
"""Decrease the account balance by amount and return the
new balance.
"""
self.transactions.append(('withdraw', amount))
if amount > self.balance:
return 'Insufficient funds'
self.balance = Account.balance - amount
return Account.balance
class Account(object):
"""A bank account that allows deposits and withdrawals.
>>> sophia_account = Account('Sophia')
>>> sophia_account.deposit(1000000) # depositing my paycheck for the week
1000000
>>> sophia_account.transactions
[('deposit', 1000000)]
>>> sophia_account.withdraw(100) # buying dinner
999900
>>> sophia_account.transactions
[('deposit', 1000000), ('withdraw', 100)]
"""
interest = 0.02
balance = 1000
def __init__(self, account_holder):
self.balance = 0
self.holder = account_holder
self.transactions = []
def deposit(self, amount):
"""Increase the account balance by amount and return the
new balance.
"""
self.transactions.append(('deposit', amount))
Account.balance = self.balance + amount
return self.balance
def withdraw(self, amount):
"""Decrease the account balance by amount and return the
new balance.
"""
self.transactions.append(('withdraw', amount))
if amount > self.balance:
return 'Insufficient funds'
self.balance = Account.balance - amount
return Account.balance
Use OK to test your code:
python3 ok -q Account
Question 2: Errors
It is often said that nothing in life is certain but death and taxes. For a programmer or data scientist, however, nothing is certain but encountering errors.
In Python, there are two primary types of errors, both of which you are likely familiar with: syntax errors and exceptions. Syntax errors occur when the proper structure of the language is not followed, while exceptions are errors that occur during the execution of a program. These include errors such as ZeroDivisionError, TypeError, NameError, and many more!
Under the hood, these errors are based in the concepts of object orientation, and all exceptions are class objects. If you're interested in more detailed explanations of the structure of exceptions as well as how to create your own, check out this article from the Python documentation! In the meantime, we'll implement our own version of an Error
class
Complete the Error
, SyntaxError
, and ZeroDivisionError
classes such that
they create the correct messages when called.
- The
SyntaxError
andZeroDivisionError
classes inherit from theError
class and add functionality that is unique to those particular errors. Their code is partially implemented for you. - The
add_code
method adds a new helpful message to your error, while thewrite
method should print the output that you see when an error is raised. - You can access the parent class methods using the super() function
class Error:
"""
>>> err1 = Error(12, "error.py")
>>> err1.write()
'error.py:12'
"""
def __init__(self, line, file):
"*** YOUR CODE HERE ***"
self.line = line
self.file = file
def write(self):
return self.file + ':' + str(self.line)
class SyntaxError(Error):
"""
>>> err1 = SyntaxError(17, "lab10.py")
>>> err1.write()
lab10.py:17 SyntaxError : Invalid syntax
>>> err1.add_code(4, "EOL while scanning string literal")
>>> err2 = SyntaxError(18, "lab10.py", 4)
>>> err2.write()
lab10.py:18 SyntaxError : EOL while scanning string literal
"""
type = 'SyntaxError'
msgs = {0 : "Invalid syntax", 1: "Unmatched parentheses", 2: "Incorrect indentation", 3: "missing colon"}
def __init__(self, line, file, code=0):
"*** YOUR CODE HERE ***"
super().__init__(line, file)
self.message = self.msgs[code]
def write(self):
end = self.type + ' : ' + self.message
"*** YOUR CODE HERE ***"
print(super().write() + " " + end)
def add_code(self, code, msg):
"*** YOUR CODE HERE ***"
self.msgs[code] = msg
class ZeroDivisionError(Error):
"""
>>> err1 = ZeroDivisionError(273, "lab10.py")
>>> err1.write()
lab10.py:273 ZeroDivisionError : division by zero
"""
type = 'ZeroDivisionError'
def __init__(self, line, file, message='division by zero'):
"*** YOUR CODE HERE ***"
super().__init__(line, file)
self.message = message
def write(self):
end = self.type + ' : ' + self.message
"*** YOUR CODE HERE ***"
print(super().write() + " " + end)
Use OK to test your code:
python3 ok -q Error
Use OK to test your code:
python3 ok -q SyntaxError
Use OK to test your code:
python3 ok -q ZeroDivisionError
Question 3: Quidditch
It's time for the opening quidditch match of the season! We represent the various positions for players with the QuidditchPlayer
class and its subclasses. Every player begins with a base_energy
level, but every position requires a different proportion of energy. Fill in the energy
method for the Beater
, Chaser
, Seeker
, and Keeper
classes, according to their docstrings. In addition, fill in the __init__
method for the Chaser
class.
class Player:
def __init__(self, name, base_energy):
"""
Players have a name, and begin with base_energy.
"""
self.name = name
self.base_energy = base_energy
def energy(self):
return self.base_energy
class Beater(QuidditchPlayer):
role = "bludgers"
def energy(self, time):
"""
Returns the amount of energy left after playing for time minutes.
After playing for time minutes, Beaters lose their base energy level
divided by the number of minutes. If time is 0, catch the ZeroDivisionError
and print "You can't divide by zero!" instead.
>>> fred = Beater("Fred Weasley", 640)
>>> fred.energy(40)
624.0
>>> fred.energy(0)
You can't divide by zero!
"""
"*** YOUR CODE HERE ***"
try:
return self.base_energy - (self.base_energy / time)
except ZeroDivisionError as e:
print("You can't divide by zero!")
Use OK to test your code:
python3 ok -q Beater.energy
class Chaser(QuidditchPlayer):
role = "score"
energy_expended = 20
def __init__(self, name, base_energy, goals):
"""
Chasers have a name, score goals, and begin with base_energy.
"""
"*** YOUR CODE HERE ***"
self.name = name
self.base_energy = base_energy
self.goals = goals
def energy(self, time):
"""
Returns the amount of energy left after playing for time minutes. For every goal
they score, they use energy_expended units of energy. In addition, they also use
10% of energy_expended if the number of minutes they have played is a multiple of 9.
>>> katie = Chaser("Katie Bell", 230, 2)
>>> katie.energy(20)
190
>>> ginny = Chaser("Ginny Weasley", 400, 3)
>>> ginny.energy(45)
338.0
"""
"*** YOUR CODE HERE ***"
energy = self.base_energy
if time % 9 == 0:
energy = energy - (0.1 * Chaser.energy_expended)
energy = energy - (self.goals * Chaser.energy_expended)
else:
energy = energy - (self.goals * Chaser.energy_expended)
return energy
Use OK to test your code:
python3 ok -q Chaser.energy
class Seeker(QuidditchPlayer):
role = "snitch"
energy_expended = 5
def energy(self, time):
"""
Returns the amount of energy after time minutes. Seekers expend energy_expended
units of their energy for every minute they have been playing.
>>> harry = Seeker("Harry Potter", 700)
>>> harry.energy(30)
550
"""
"*** YOUR CODE HERE ***"
return self.base_energy - (time * Seeker.energy_expended)
Use OK to test your code:
python3 ok -q Seeker.energy
class Keeper(QuidditchPlayer):
role = "guard"
energy_expended = 50
def energy(self, time):
"""
Returns the amount of energy after time minutes. If less than 30 minutes have
passed, then Keepers do not lose any energy. If 30 minutes or more have passed,
then Keepers expend 80% of their energy_expended units for every full 15
minutes that pass.
>>> oliver = Keeper("Oliver Wood", 380)
>>> oliver.energy(45)
260.0
"""
"*** YOUR CODE HERE ***"
energy = self.base_energy
if time < 30:
return self.base_energy
else:
for i in range(time // 15):
energy = energy - (0.8 * Keeper.energy_expended)
return energy
Use OK to test your code:
python3 ok -q Keeper.energy
After you finish implementing the QuidditchPlayers, run the following command in your terminal to play the game:
python3 -i quidditch_game.py
Preparing for Project 2: Debugging
Question 4: Werewolf
Now, you want to play a game, inspired by Werewolf, with a group of your friends. A Pl88yer
can either be a Werewolf
, or a Villager
. In this game, you can have 4 or more players. The first 2 players are automatically designated to be Werewolves, while everyone else is assigned to be a Villager. One play of the game involves all of the players voting for a player who they believe to be a Werewolf; in this implementation, all players, except yourself, arbitrarily vote for themselves. At the end of each play, the player with the most votes is removed from the game. Each play of the game alternates between daytime and nighttime. If it is nighttime, only votes by werewolves count.
The game ends when there are no werewolves left, which means the villagers won, or when there are more werewolves than villagers, which means the werewolves have won.
However, there are bugs in the code for the game! Read through the code and fix all of the bugs so that the game can work properly. As hint, there are at least four bugs in the code! For this lab there is a hidden test. In other words, we will be running a test against your code that you don't have access to, so make sure to be thorough! You should not need to add any lines - only edit existing lines.
def get_most_common_element(lst):
return max(set(lst), key=lst.count)
class Pl88yer:
def __init__(self, name):
self.name = name
self.active = True
class Werewolf(Pl88yer):
def __init__(self, name):
Pl88yer.__init__(self, name)
def reveal_player_type(self):
print("You are a werewolf!")
class Villager(Pl88yer):
def __init__(self, name):
Villager.__init__(self, name)
def reveal_player_type(self):
print("You are a villager!")
class WerewolfGame:
def __init__(self, players, your_name):
"""
Sets the game up. players is a list of strings that are names of all
of the players. your_name is a string and must be one of the players.
>>> game = WerewolfGame(["a", "b", "c", "d", "e", "f"], "a")
You are a werewolf!
>>> game.your_name
'a'
>>> game.play("b")
'Keep playing!'
>>> len(game.werewolves)
1
>>> len(game.villagers)
4
>>> game.play("c")
'Keep playing!'
>>> game.play("d")
'Keep playing!'
>>> game.play("a")
'Villagers win!'
>>> game.werewolves
[]
>>> len(game.villagers)
2
"""
if len(players) < 4:
raise Exception("Not enough players!")
names = players[0:2]
self.your_name = your_name
self.werewolves = [Werewolf(self, w) for w in names]
self.villagers = [Villager(self, p) for p in players if p not in names]
self.name_to_player = {}
for werewolf in self.werewolves:
self.name_to_player[werewolf.name] = werewolf
for villager in self.villagers:
self.name_to_player[villager.name] = villager
player = self.name_to_player[your_name]
player.reveal_player_type()
self.state = "night"
def play(self, vote):
"""
While the game is still being played, make a move. vote is the player
who you vote for, because you believe they are on the opposing team.
You can continue playing until either the villagers or the werewolves win.
"""
self.make_move(vote)
if not self.check_if_end_of_game():
return "Keep playing!"
else:
if len(self.werewolves) == 0:
return "Villagers win!"
elif len(self.werewolves) > len(self.villagers):
return "Werewolves win!"
def make_move(self, vote):
"""
Every player votes (players arbitrarily vote for themselves). Then,
if the state of the game is day, remove the player with the most votes
overall, and set the state to night. If the state of the game is night,
remove the player with the most votes by werewolves, and set the state to day.
"""
votes = []
werewolf_votes = []
if self.state == "night":
werewolf_votes.append(vote)
votes.append(vote)
for player in self.name_to_player:
if self.state == "night" and isinstance(player, Werewolf(name)):
werewolf_votes.append(player)
votes.append(player)
if self.state == "day":
majority_vote = get_most_common_element(votes)
self.state = "night"
elif self.state == "night":
majority_vote = get_most_common_element(werewolf_votes)
self.state = "day"
if majority_vote in self.name_to_player:
self.remove_player(majority_vote)
else:
raise Exception("Invalid player.")
def remove_player(player_to_remove):
"""
Set the player with the majority vote to inactive, and remove it from
its respective list of players.
"""
player = self.name_to_player[player_to_remove]
self.active = False
if player in self.werewolves:
self.werewolves.remove(player)
elif player in self.villagers:
self.villagers.remove(player)
else:
print("Player already removed!")
def check_if_end_of_game(self):
"""
Returns True if the game is over, and False if it is not. The game is over when
there are no werewolves remaining, or if there are more werewolves than villagers.
"""
if len(WerewolfGame.werewolves) == 0:
return True
elif len(WerewolfGame.werewolves) > len(WerewolfGame.villagers):
return True
else:
return False
Run the following command in your terminal to play around with your code:
python3 -i lab10.py
After running the above command, enter the below line to actually start a game and play through the code that you've debugged. We highly recommend you do this a couple times to test your code and make sure it's bug free. If you need a refresher on the commands for interacting with the game, refer to the doctests for this question!
game = WerewolfGame(["a", "b", "c", "d", "e", "f"], "a")
def get_most_common_element(lst):
return max(set(lst), key=lst.count)
class Pl88yer:
def __init__(self, name):
self.name = name
self.active = True
class Werewolf(Pl88yer):
def __init__(self, name):
Pl88yer.__init__(self, name)
def reveal_player_type(self):
print("You are a werewolf!")
class Villager(Pl88yer):
def __init__(self, name):
Villager.__init__(self, name)
def reveal_player_type(self):
print("You are a villager!")
class WerewolfGame:
def __init__(self, players, your_name):
"""
Sets the game up. players is a list of strings that are names of all
of the players. your_name is a string and must be one of the players.
>>> game = WerewolfGame(["a", "b", "c", "d", "e", "f"], "a")
You are a werewolf!
>>> game.your_name
'a'
>>> game.play("b")
'Keep playing!'
>>> len(game.werewolves)
1
>>> len(game.villagers)
4
>>> game.play("c")
'Keep playing!'
>>> game.play("d")
'Keep playing!'
>>> game.play("a")
'Villagers win!'
>>> game.werewolves
[]
>>> len(game.villagers)
2
"""
if len(players) < 4:
raise Exception("Not enough players!")
names = players[0:2]
self.your_name = your_name
self.werewolves = [Werewolf(self, w) for w in names]
self.villagers = [Villager(self, p) for p in players if p not in names]
self.name_to_player = {}
for werewolf in self.werewolves:
self.name_to_player[werewolf.name] = werewolf
for villager in self.villagers:
self.name_to_player[villager.name] = villager
player = self.name_to_player[your_name]
player.reveal_player_type()
self.state = "night"
def play(self, vote):
"""
While the game is still being played, make a move. vote is the player
who you vote for, because you believe they are on the opposing team.
You can continue playing until either the villagers or the werewolves win.
"""
self.make_move(vote)
if not self.check_if_end_of_game():
return "Keep playing!"
else:
if len(self.werewolves) == 0:
return "Villagers win!"
elif len(self.werewolves) > len(self.villagers):
return "Werewolves win!"
def make_move(self, vote):
"""
Every player votes (players arbitrarily vote for themselves). Then,
if the state of the game is day, remove the player with the most votes
overall, and set the state to night. If the state of the game is night,
remove the player with the most votes by werewolves, and set the state to day.
"""
votes = []
werewolf_votes = []
if self.state == "night":
werewolf_votes.append(vote)
votes.append(vote)
for player in self.name_to_player:
if self.state == "night" and isinstance(player, Werewolf(name)):
werewolf_votes.append(player)
votes.append(player)
if self.state == "day":
majority_vote = get_most_common_element(votes)
self.state = "night"
elif self.state == "night":
majority_vote = get_most_common_element(werewolf_votes)
self.state = "day"
if majority_vote in self.name_to_player:
self.remove_player(majority_vote)
else:
raise Exception("Invalid player.")
def remove_player(player_to_remove):
"""
Set the player with the majority vote to inactive, and remove it from
its respective list of players.
"""
player = self.name_to_player[player_to_remove]
self.active = False
if player in self.werewolves:
self.werewolves.remove(player)
elif player in self.villagers:
self.villagers.remove(player)
else:
print("Player already removed!")
def check_if_end_of_game(self):
"""
Returns True if the game is over, and False if it is not. The game is over when
there are no werewolves remaining, or if there are more werewolves than villagers.
"""
if len(WerewolfGame.werewolves) == 0:
return True
elif len(WerewolfGame.werewolves) > len(WerewolfGame.villagers):
return True
else:
return False
There are no OK tests for this question! However, it will still be graded as part of the lab, so be sure that your solution works before submitting!
Submit
Make sure to submit this assignment by running:
python3 ok --submit
Extra Credit Practice Open in a new window
These questions are new this semester. They're a mix of Parsons Problems, Code Tracing questions, and Code Writing questions.
Confused about how to use the tool? Check out https://codestyle.herokuapp.com/cs88-lab01 for some problems designed to demonstrate how to solve these types of problems.
These cover some similar material to lab, so can be helpful to further review or try to learn the material. Unlike lab and homework, after you've worked for long enough and tested your code enough times on any of these questions, you'll have the option to view an instructor solution. You'll unlock each question one at a time, either by correctly answering the previous question or by viewing an instructor solution.
Starting from lab 2 onward, each set of questions are worth half (0.5) a point per lab, for a total opportunity of 4-5 points throughout the semester.
Use OK to test your code:
python3 ok -q extra_credit