A document from MCS 275 Spring 2022, instructor David Dumas. You can also get the notebook file.

MCS 275 Spring 2022 Worksheet 3 Solutions

  • Course instructor: David Dumas
  • Solutions prepared by: Jennifer Vaccaro, Johnny Joyce

Topics

This worksheet focuses on object-oriented programming.

Resources

These things might be helpful while working on the problems. Remember that for worksheets, we don't strictly limit what resources you can consult, so these are only suggestions.

1. Get the Project 1 starter pack

Project 1 is due on 4 February. To prepare for working on it, please download and extract ths starter pack, which is a ZIP file:

You don't just want to view the contents of the ZIP file in Windows explorer; it's important to actually extract the files so they exist in a directory where you can do your project work.

When you've extracted the starter pack, check that you know the location of textsimulation.py and that you can run it in the terminal.

The point of asking you to do it during lab is to ensure the TA can help you if you run into any problems.

2. Additional bots

Download these files related to the robot simulation from the course sample code repository and put them in a directory where you'll do your work for this problem.

Then, build these new robots in bots.py that are subclasses of Bot:

  • class DelayMarchBot()

    • A robot that waits for a specified number of time units, and thereafter marches in a direction (default is Vector(1,0), but any direction can be specified in the constructor)
  • class PauseMarchBot()

    • At each time step, the robot chooses one of these two things to do based on a coin flip (random choice):
      • Take a step in a fixed direction that was set in the constructor
      • Pause for a moment (do nothing)
  • class TeleportBot()
    • Most of the time, this robot just stands still. Buit at each time step there is a 10% chance that it will teleport to a random location.

Add these robots to the simulation and confirm they exhibit the expected behavior.

Solution

New additions to bots.py:

In [ ]:
class DelayMarchBot(Bot):
    """Robot that waits, and then proceeds at a constant pace in a consistent direction"""
    def __init__(self, position, direction=plane.Vector2(1,0), wait_time=0):
        """Constructor sets up the bot with a position, direction, and wait_time"""
        super().__init__(position)
        self.direction = direction
        self.wait_time = wait_time
    def update(self):
        """If wait_time>0, waits. Otherwise, march in a consistent direction"""
        if self.wait_time>0:
            self.wait_time -= 1 # wait for an update cycle
        else:
            self.position += self.direction # after the waiting is done, march!

class PauseMarchBot(Bot):
    """Robot that either waits or marches, based on a random choice"""
    def __init__(self, position, direction=plane.Vector2(1,0)):
        """Constructor sets up the bot with a position and direction"""
        super().__init__(position)
        self.direction = direction
    def update(self):
        """Decides using random.choice whether to pause or march"""
        choice = random.choice(["pause", "march"])
        if choice == "march":
              self.position += self.direction
        # otherwise, if choice == "pause", do nothing
        

        
class TeleportBot(Bot):
    """Stands still 90% of the time. Teleports randomly 10% of the time."""
    
    def __init__(self, position, width=60, height=30):
        """Constructor sets up the bot at given position. Also saves width & height of plane"""
        super().__init__(position)
        self.width = width
        self.height = height
        
    def update(self):
        """Decides using random.choice whether to pause or march"""
        
        # random.random() generates random number between 0 and 1
        if random.random() < 0.9: # 90% chance
            pass # Do nothing
        else: # 10% chance
            # Build a Point2 object for a new, random position
            newpos = plane.Point2(
                random.randrange(self.width),  # random element of range(self.width)
                random.randrange(self.height)
            )
            self.position = newpos

3. Cipher class hierarchy

Build a module encoders (in encoders.py) containing classes for simple ciphers (or codes; ways of obscuring the contents of a string that can be undone later by the intended recipient).

There should be a base class BaseEncoder that has two methods:

  • encode(self,text) : Returns the string text unchanged. Subclasses will alter this behavior.
  • decode(self,text) : Returns the string text unchanged. Subclasses will alter this behavior.

It should be the case that obj.decode(obj.encode(s)) == s is true for any string s, and for any object obj that is an instance of BaseEncoder or subclass thereof.

Then, build subclasses of BaseEncoder that implement encoding and decoding by different ciphers, including:

  • RotateEncoder : Encoding rotates letters in the alphabet forward by a certain number of steps, e.g. so rotation by 5 turns "a" into "f" and "z" into "e" (because we wrap around when we reach the end of the alphabet). No transformation is applied to characters other than capital and lower case letters. Constructor accepts an integer, specifying the number of steps to rotate.

  • Rot13Encoder : A subclass of RotateEncoder that fixes the steps at 13, so that encoding and decoding are the same operation.

  • SubstitutionEncoder : The constructor accepts two arguments, pre and post. The string pre is a list of characters to be replaced when encoding, and string post indicates the things to replace them with. For example, using pre="abcd" and post="1j4e" would mean that "a" is supposed to be replaced by "1", "b" by "j", "c" by "4", and so on.

    • Be careful writing the encoder so that you don't replace things twice. For example pre="abc" and post="bca" should encode "banana" to "cbnbnb", and not "ccncnc".
    • You can assume that pre and post contain the same characters but in a different order. If that's not the case, then it would be impossible to ensure that decoding after encoding always gives the original text back again.

You can find some test code below. The test code assumes all of the classes are in the global scope.

In [ ]:
E = RotateEncoder(5)
s = E.encode("Hello world!") # Mjqqt btwqi!
print(s) # Mjqqt btwqi!
print(E.decode(s)) # Hello world!

F = SubstitutionEncoder("lmno","nolm")
s = F.encode("Hello everyone!")
print(s) # Hennm everymle!
print(F.decode(s)) # Hello everyone!

Solution

Code containing the Encoder classes:

In [2]:
class BaseEncoder():
    """Base class for simple encoders"""
    
    def encode(self, text):
        """Base class returns string unchanged"""
        return text
    
    def decode(self, text):
        """Base class returns string unchanged"""
        return text

    
    
class SubstitutionEncoder():
    """Substitutes one string of text for another"""
    
    def __init__(self, pre, post):
        """Store attributes of characters to be replaced"""
        self.pre = pre
        self.post = post

    def encode(self, text):
        """Encodes the test using the rotate encoder"""
        text_encoded = ""
        for c in text:
            if c in self.pre:
                text_encoded += self.post[self.pre.index(c)] # substitute with the corresponding letter from post
            else:
                text_encoded += c # or add c, unchanged
        return text_encoded

    def decode(self, text):
        """Decodes the text using the rotate encoder"""
        text_decoded = ""
        for c in text:
            if c in self.post:
                text_decoded += self.pre[self.post.index(c)] # substitute with the corresponding letter from pre
            else:
                text_decoded += c # or add c, unchanged
        return text_decoded

    
    
    
class RotateEncoder(SubstitutionEncoder):
    """Encoding rotates alphabet letters forward by a certain number of steps"""
    
    def __init__(self, num_steps):
        """Constructor sets the integer number of steps to rotate by"""

        # Create the alphabet substitution blocks
        abc_upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        abc_lower = abc_upper.lower()

        # pre is the alphabet, with all upper/lowercase letters in order
        pre = abc_upper + abc_lower
        # post is a "shuffled" version of the alphabets, offset by n
        post = abc_upper[num_steps:] + abc_upper[:num_steps] + abc_lower[num_steps:] + abc_lower[:num_steps]

        # Use pre and post as inputs in the superclass SubstitutionEncoder constructor
        # Then you don't need to write custom encode/decode functions
        super().__init__(pre, post)

        
        
        
class Rot13Encoder(RotateEncoder):
    """Encoding rotates alphabet letter by 13 steps"""
    
    def __init__(self):
        """Constructor calls super class RotateEncoder with num_steps 13"""

        # Use 13 as input in the superclass RotateEncoder constructor
        # Then you don't need to write custom encode/decode functions
        super().__init__(13)

Code running the Encoders:

In [3]:
E = RotateEncoder(5)
s = E.encode("Hello world!") # Mjqqt btwqi!
print(s) # Mjqqt btwqi!
print(E.decode(s)) # Hello world!

F = SubstitutionEncoder("lmno","nolm")
s = F.encode("Hello everyone!")
print(s) # Hennm everymle!
print(F.decode(s)) # Hello everyone!
Mjqqt btwqi!
Hello world!
Hennm everymle!
Hello everyone!

Bonus round

Work on these open-ended problems if you finish the exercises above. We don't plan to include solutions to these in the worksheet solutions, but we may do so if most people end up working on any of these.

WORM dictionary

Write a subclass of dict that only lets you set the value associated with a key once. If you try to change the value associated with an existing key, it raises ValueError. You'll need to read up on the special method __setitem__ to make this work.

(WORM stands for Write Once, Read Many times.)

Sending a key

The encoders in problem 3 don't handle the problem of communicating to your message recipient the information about what code you will use for future messages.

Add __str__ and __repr__ methods to the ciphers that give enough information so that a message recipient who is given encoded text and the return value of str(encoder_object) would be able to instantiate an encoder and decode a message.

More interesting cipher

Design and implement another cipher as a subclass of BaseEncoder which isn't as simple as substituting letters with specified replacements. For example, you might make it so that the way a letter is handled depends on both the letter and the text that's been encoded so far. Confirm that your cipher

DelayedActionBot

Make a robot class (a subclass of DestructBot) that stands still for a specified number of steps and then self-destructs. But before it does so, this class calls a user-specified function. The function is given as an argument to the constructor. So, for example:

def bye():
    """Robot says goodbye"""
    print("Thanks for including me in this simulation.  I was glad to be written in Python.  Goodbye.")

R = bots.DelayedActionBot(position=Point(3,3),lifetime=10,action=bye)

# ... code to run the simulation ...

should make a robot that sits at position (3,3) for 10 steps, prints a message, and then self-destructs.

The action argument of the constructor should default to None, and the class should know to not do anything if action==None. That way, any code that works with DestructBot will also work with DelayedActionBot.

Solution: WORM dictionary

In [2]:
class WriteOnceDict(dict):
    """
    Dictionary where an existing value cannot be changed.
    """
    def __setitem__(self,k,v):
        "Create new key `k` with value `v`; if `k` is already a key, raise ValueError"
        if k in self:
            raise ValueError("Attempt to change value associated to existing key {}.  This is not allowed by {}.".format(
                k,
                self.__class__.__name__ # name of this class, as a string
            ))
        else:
            super().__setitem__(k,v) # Call `dict` class setitem method
                                     # Note: can't just say self[k]=v since that will call this method!
                                     # Also, super() doesn't allow item assignment, e.g. super()[k]=v fails.
            
d = WriteOnceDict({"first_key":"first_value"}) # Create write once dict containing 1 key and 1 corresponding value
print(d)

d["second_key"] = "second_value" # Verify that we can add a brand new key:value pair without any issues
print(d)

d["second_key"] = "updated_value" # Raises ValueError when we try to rewrite mykey (as expected)
{'first_key': 'first_value'}
{'first_key': 'first_value', 'second_key': 'second_value'}
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-2-3d0b91da62bc> in <module>
     21 print(d)
     22 
---> 23 d["second_key"] = "updated_value" # Raises ValueError when we try to rewrite mykey (as expected)

<ipython-input-2-3d0b91da62bc> in __setitem__(self, k, v)
      6         "Create new key `k` with value `v`; if `k` is already a key, raise ValueError"
      7         if k in self:
----> 8             raise ValueError("Attempt to change value associated to existing key {}.  This is not allowed by {}.".format(
      9                 k,
     10                 self.__class__.__name__ # name of this class, as a string

ValueError: Attempt to change value associated to existing key second_key.  This is not allowed by WriteOnceDict.