Due at 11:59:59 pm on Thursday, 11/26/2019.

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 to ok.

Inheritance

Let's implement a game called Election. In this game, two players compete to try and earn the most votes. Both players start with 0 votes and 100 popularity.

The two players alternate turns, and the first player starts. Each turn, the current player chooses an action. There are two types of actions:

  • The player can debate, and either gain or lose 50 popularity. If the player has popularity p1 and the other player has popularity p2, then the probability that the player gains 50 popularity is max(0.1, p1 / (p1 + p2)) Note that the max causes the probability to never be lower than 0.1.
  • The player can give a speech. If the player has popularity p1 and the other player has popularity p2, then the player gains p1 // 10 votes and popularity and the other player loses p2 // 10 popularity.

The game ends when a player reaches 50 votes, or after a total of 10 turns have been played (each player has taken 5 turns). Whoever has more votes at the end of the game is the winner!

Question 1: Player

First, let's implement the Player class. Fill in the debate and speech methods, that take in another Player other, and implement the correct behavior as detailed above. Here are two additional things to keep in mind:

  • In the debate method, you should call the provided random function, which returns a random float between 0 and 1. The player should gain 50 popularity if the random number is smaller than the probability described above, and lose 50 popularity otherwise.
  • Neither players' votes or popularity should ever become negative. If this happens, set it equal to 0 instead.
  • This problem has additional hidden doc tests that will test full correctness.
### Phase 1: The Player Class
class Player:
    """
    >>> random = make_test_random()
    >>> p1 = Player('Hill')
    >>> p2 = Player('Don')
    >>> p1.popularity
    100
    >>> p1.debate(p2)  # random() should return 0.0
    >>> p1.popularity
    150
    >>> p2.popularity
    100
    >>> p2.votes
    0
    >>> p2.speech(p1)
    >>> p2.votes
    10
    >>> p2.popularity
    110
    >>> p1.popularity
    135

>>> # Additional correctness tests >>> p1.speech(p2) >>> p1.votes 13 >>> p1.popularity 148 >>> p2.votes 10 >>> p2.popularity 99 >>> for _ in range(4): # 0.1, 0.2, 0.3, 0.4 ... p1.debate(p2) >>> p2.debate(p1) >>> p2.popularity 49 >>> p2.debate(p1) >>> p2.popularity 0
""" def __init__(self, name): self.name = name self.votes = 0 self.popularity = 100 def debate(self, other):
"*** YOUR CODE HERE ***"
prob = max(0.1, self.popularity / (self.popularity + other.popularity)) if random() < prob: self.popularity += 50 else: self.popularity = max(0, self.popularity - 50)
def speech(self, other):
"*** YOUR CODE HERE ***"
self.votes += self.popularity // 10 self.popularity += self.popularity // 10 other.popularity -= other.popularity // 10
def choose(self, other): return self.speech

Use OK to test your code:

python3 ok -q Player

Question 2: Game

Now, implement the Game class. Fill in the play method, which should alternate between the two players, starting with p1, and have each player take one turn at a time. The choose method in the Player class returns the method, either debate or speech, that should be called to perform the action.

In addition, fill in the winner property method, which should return the player with more votes, or None if the players are tied. This problem has additional hidden doc tests that will test full correctness.

### Phase 2: The Game Class
class Game:
    """
    >>> p1, p2 = Player('Hill'), Player('Don')
    >>> g = Game(p1, p2)
    >>> winner = g.play()
    >>> p1 is winner
    True

>>> # Additional correctness tests >>> winner is g.winner True >>> g.turn 10 >>> p1.votes = p2.votes >>> print(g.winner) None
""" def __init__(self, player1, player2): self.p1 = player1 self.p2 = player2 self.turn = 0 def play(self): while not self.game_over:
"*** YOUR CODE HERE ***"
if self.turn % 2 == 0: curr, other = self.p1, self.p2 else: curr, other = self.p2, self.p1 curr.choose(other)(other) self.turn += 1
return self.winner @property def game_over(self): return max(self.p1.votes, self.p2.votes) >= 50 or self.turn >= 10 @property def winner(self):
"*** YOUR CODE HERE ***"
if self.p1.votes > self.p2.votes: return self.p1 elif self.p2.votes > self.p1.votes: return self.p2 else: return None

Use OK to test your code:

python3 ok -q Game

Question 3: New Players

The choose method in the Player class is boring, because it always returns the speech method. Let's implement two new classes that inherit from Player, but have more interesting choose methods.

Implement the choose method in the AggressivePlayer class, which returns the debate method if the player's popularity is less than or equal to other's popularity, and speech otherwise. Also implement the choose method in the CautiousPlayer class, which returns the debate method if the player's popularity is 0, and speech otherwise. This problem has additional hidden doc tests that will test full correctness.

### Phase 3: New Players
class AggressivePlayer(Player):
    """
    >>> random = make_test_random()
    >>> p1, p2 = AggressivePlayer('Don'), Player('Hill')
    >>> g = Game(p1, p2)
    >>> winner = g.play()
    >>> p1 is winner
    True

>>> # Additional correctness tests >>> p1.popularity = p2.popularity >>> p1.choose(p2) == p1.debate True >>> p1.popularity += 1 >>> p1.choose(p2) == p1.debate False >>> p2.choose(p1) == p2.speech True
""" def choose(self, other):
"*** YOUR CODE HERE ***"
if self.popularity <= other.popularity: return self.debate else: return self.speech
class CautiousPlayer(Player): """ >>> random = make_test_random() >>> p1, p2 = CautiousPlayer('Hill'), AggressivePlayer('Don') >>> p1.popularity = 0 >>> p1.choose(p2) == p1.debate True >>> p1.popularity = 1 >>> p1.choose(p2) == p1.debate False
>>> # Additional correctness tests >>> p2.choose(p1) == p2.speech True
""" def choose(self, other):
"*** YOUR CODE HERE ***"
if self.popularity == 0: return self.debate else: return self.speech

Use OK to test your code:

python3 ok -q AggressivePlayer
python3 ok -q CautiousPlayer

Question 4: 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 5: 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!