Assignment 2 - Keeping Secrets

Due: Thursday, April 4, 2024, at 10pm

You may work alone or with a partner, but you must type up the code yourself. You may also discuss the assignment at a high level with other students. You should list any student with whom you discussed each part, and the manner of discussion (high-level, partner, etc.) in a comment at the top of each file. You should only have one partner for an entire assignment.

You should submit your assignment as an a2.zip file on Moodle.


Parts of this assignment:


Comments and collaboration

As with all assignments in this course, for each file in this assignment, you are expected to provide top-level comments (lines that start with # at the top of the file) with your name and a collaboration statement. For this assignment, you have multiple programs; each needs a similar prelude.

You need a collaboration statement, even if just to say that you worked alone.

Here is an example of how you can start a file:

# File: popChange.py
# Purpose: Models the population of Azumarill over time.
# Author: Lulu Amert
#
# Collaboration statement:
# - Milo: partner (worked on code together)
# - Hobbes: discussed issues with spaces in print() output
# - Slack: link to Python documentation for print()

Here is another example:

# File: testingRandom.py
# Purpose: Generates a user-specified number of integers within a user-specified
#          range and calculates some statistics for those numbers.
# Author: Hobbes Amert
#
# Collaboration statement: I worked alone.
#
# Inputs:
#   * number of integers to generate (int)
#   * bottom of range (int)
#   * top of range (int)

Part 1: Modeling Azumarill changes

# You should be equipped to complete this part after Lesson 3 (Friday Mar. 29).

Rabbits are said to have exponential population growth. To see if this applies to rabbit Pokemon, such as Azumarill, let’s write a program to model the changes in the Azumarill population, given an initial count.

Note that you can’t have fractional Azumarill, so you should round the population after each time step.

Your program should prompt the user for:

  • the initial number of Azumarill,
  • the birth rate (a number between 0 and 1, representing the percentage born each time step; a rate of 0.2 means there are 20% more Azumarill after one time step), and
  • how many time steps to simulate.

Here is some “skeleton code” to get you started:

# File: popChange.py
# Purpose: Models the population of Azumarill over time.
# Author: TODO
#
# Collaboration statement: TODO
#
# Inputs:
#   * initial Azumarill population (int)
#   * birth rate (float between 0 and 1)
#   * number of time steps (int)

def main():
    # Get the inputs from the user
    ## TODO: Your code here

    # Convert the inputs to the correct types
    ## TODO: Your code here

    print("Initial Azumarill population count:", initial_pop) # TODO: define to avoid a NameError

    # Prep the accumulator variable
    pop = initial_pop # this will change!

    # Loop over the time steps, updating the population
    ## TODO: the rest of your code here (this one's a for loop)

    print("Final Azumarill population count:", pop)

main()

Here are some example interactions that you should mimic:

What is the initial population? 50
What is the birth rate? 0.1
How many time steps? 10
Initial Azumarill population count: 50
Final Azumarill population count: 131
What is the initial population? 20
What is the birth rate? 0.25
How many time steps? 40
Initial Azumarill population count: 20
Final Azumarill population count: 149274

Part 2: The random library

# You should be equipped to complete this part after Lesson 3 (Friday Mar. 29).

Similar to the math library, Python provides some useful functionality for pseudo-random numbers in a library called random.

Here are some example functions, taken from the Python documentation:

import random                      # import the random library

# Fun with single random values
val = random.random()              # random float:  0.0 <= x < 1.0
print(val)                         # what I got: 0.37444887175646646

val = random.uniform(2.5, 10.0)    # random float:  2.5 <= x < 10.0
print(val)                         # what I got: 3.1800146073117523

val = random.randrange(10)         # integer from 0 to 9 inclusive
print(val)                         # what I got: 7

val = random.randrange(0, 101, 2)  # *even* integer from 0 to 100 inclusive
print(val)                         # what I got: 26

# Now playing with lists!
val = random.choice(['win', 'lose', 'draw'])   # single random element from a sequence
print(val)                                     # what I got: draw
                                               # note that it doesn't print the ''

deck = 'ace two three four'.split()            # nifty string method to make a list
random.shuffle(deck)                           # shuffle a list
print(deck)                                    # what I got: ['four', 'two', 'ace', 'three']

val = random.sample([10, 20, 30, 40, 50], k=4) # four samples without replacement
print(val)                                     # what I got: [40, 10, 50, 30]

The random-number generators used by the random module are not truly random. Rather, they are pseudo random (so don’t ever use them to write security software!). We can initialize the random-number generators using the seed function; if you do this with the same seed each time, you get reproduceable results. This can be especially handy for debugging.

import random

# Set the seed, then print 5 random numbers between 0 and 1
random.seed(111)
for i in range(5):
    random.random()

# Example output:
# 0.827170565342314
# 0.21276311517617263
# 0.9425194436011797
# 0.49391971673226975
# 0.3975871534419906

# Set a new seed, then do it again:
random.seed(0)
for i in range(5):
    random.random()

# Example output:
# 0.8444218515250481
# 0.7579544029403025
# 0.420571580830845
# 0.25891675029296335
# 0.5112747213686085

# Let's go back to the original seed
random.seed(111)
for i in range(5):
    random.random()

# Example output (it's the same as the first time :o)
# 0.827170565342314
# 0.21276311517617263
# 0.9425194436011797
# 0.49391971673226975
# 0.3975871534419906

To make something closer to actual randomness, you an call seed without any arguments (like this: random.seed()). In that case, it uses the computer’s system time, which is not constant.

Trying out pseudo-randomness

Your friend doesn’t think that Python’s random library is that random. To prove that it behaves fairly randomly, you decide to write a program to prove them wrong (or at least, prove as much as you can after your first week of CS 111).

You decide to compute the following metrics:

  • minimum
  • maximum
  • average

Your program should generate a user-specified number of random integers and compute some statistics on them. You should also ask the user for a maximum and minimum integer. Put your code in a file called testingRandom.py.

(Remember: Python has built-in functions min(..) and max(..) that you might find helpful. Here is a link to the documentation: https://docs.python.org/3/library/functions.html.)

Note: For full credit, you must use min/max and not use any if statements or lists, even if you already know about them from prior CS experience.

Here is some “skeleton code” to get you started:

# File: testingRandom.py
# Purpose: Generates a user-specified number of integers within a user-specified
#          range and calculates some statistics for those numbers.
# Author: TODO
#
# Collaboration statement: TODO
#
# Inputs:
#   * number of integers to generate (int)
#   * bottom of range (int)
#   * top of range (int)

import random

def main():
    numInts = int(input("How many integers should I generate? "))

    ## TODO: the rest of your code here

    print() # blank line
    print("Statistics:")
    print("The minimum value was", minSeen)
    print("The maximum value was", maxSeen)
    print("The average value was", averageSeen)

main()

Here is some example output:

How many integers should I generate? 40
What is the minimum integer? 20
What is the maximum integer? 80

Statistics:
The minimum value was 25
The maximum value was 80
The average value was 51.625

Here is more, with more data points (one million data points can take a few seconds to run):

How many integers should I generate? 1000000
What is the minimum integer? 20
What is the maximum integer? 80

Statistics:
The minimum value was 20
The maximum value was 80
The average value was 49.98235

Part 3: A Caesar cipher

# You should be equipped to complete this entire part after Lesson 4 (Monday Apr. 1).

3a: Keeping simple secrets

A Caesar cipher is a simple substitution cipher based on the idea of shifting each letter of the plaintext message a fixed number (called the key) of positions in the alphabet. For example, if the key value is 2, the word “Banana” would be encoded as “Dcpcpc”. The original message can be recovered by “re-encoding” it using the negative of the key (e.g., -2).

Write a program (simpleCaesar.py) that can encode and decode Caesar ciphers. The input to the program will be a string of plaintext and the value of the key. The output will be an encoded message where each character in the original message is replaced by shifting it key characters in the Unicode character set.

Here is some starter code:

# File: simpleCaesar.py
# Purpose: Encodes or decodes a message using a Caesar cipher.
# Author: TODO
#
# Collaboration statement: TODO
#
# Inputs:
#   * string to encode/decode (str)
#   * key (int)

def main():
    # Get the plaintext message and the key from the user
    plaintext = # TODO
    key = # TODO

    # Initialize the variable to store the encrypted message in
    msg = # TODO

    # Build the encrypted message using the accumulator pattern
    # TODO

    # Display the result to the user
    print("The encrypted message is:\n" + msg)

main()

Here is some example output:

Please enter a string to encrypt: Apple banana cat dog elephant fish
Please enter a key to shift by (an integer): 10

The encrypted message is:
Kzzvo*lkxkxk*mk~*nyq*ovozrkx~*ps}r

We can check that decryption works, too:

Please enter a string to encrypt: Kzzvo*lkxkxk*mk~*nyq*ovozrkx~*ps}r
Please enter a key to shift by (an integer): -10

The encrypted message is:
Apple banana cat dog elephant fish

3b: Going around in circles

One problem with the program in Part 3a is that it does not deal with the case when we “drop off the end” of the alphabet. A true Caesar cipher does the shifting in a circular fashion where the next character after “z” is “a”.

With a key of 1, you should have these shifts:

Original -> Shifted
a -> b
z -> A
A -> B
Z -> (space)
(space) -> a

Copy your solution from Part 3a to a new file called circularCaesar.py. Modify this code to make it circular. You may assume that the input consists only of English letters (uppercase and lowercase) and spaces.

(Hint: Make a string containing all of the characters of your alphabet and use positions in this string as your code, rather than using ord and chr. You might find the string function index helpful. Also, there are constants in the string module that might be of help to you…)

Here is some example output for this new cipher program.

Please enter a string to encrypt: Apple banana cat dog elephant fish
Please enter a key to shift by (an integer): 10

The encrypted message is:
KzzvojlkxkxkjmkDjnyqjovozrkxDjpsCr
Please enter a string to encrypt: KzzvojlkxkxkjmkDjnyqjovozrkxDjpsCr
Please enter a key to shift by (an integer): -10

The encrypted message is:
Apple banana cat dog elephant fish

Reflection

# You should be equipped to complete this part after finishing your assignment.

Were there any particular issues or challenges you dealt with in completing this assignment? How long did you spend on this assignment? Write a brief discussion (a sentence or two is fine) in your readme.txt file.

Here are some examples:

##### Reflection #####
# I kept getting 130 Azumarill instead of 131 because I forgot to round each time step.
# I had issues figuring out how to move away from chr() and ord() for 3b.
# I had to look up the Python string documentation.
# I spent 9 hours on this assignment.
##### Reflection #####
# I started late, so I had to rush to make sure I knew how to make a .zip file.
# It may be good to start early next time.
# I spent 7 hours on this assignment.
##### Reflection #####
# It went fine; I found what I needed in my notes.
# I spent 5 hours on this assignment.

Grading

This assignment will be graded out of 100 points, as follows:

  • 5 points - submit a valid a2.zip file with all files correctly named

  • 5 points - all code files contain top-level comments with file name, purpose, and author names

  • 5 points - all code files’ top-level comments contain collaboration statement

  • 20 points - popChange.py program asks user for inputs (6 pts), correctly computes the final population (8 pts), prints the result (3 pts), and matches the example interaction (3 pts)

  • 20 points - testingRandom.py program asks user for inputs (4 pts), correctly computes statistics (5 pts), prints the results (3 pts), matches the example interaction (3 pts), and doesn’t use lists or if statements (5 pts)

  • 20 points - simpleCaesar.py program asks user for inputs (5 pts), converts each character of the message (5 pts), builds the resulting string (4 pts), prints the results (3 pts), and matches the example interaction (3 pts)

  • 20 points - circularCaesar.py program asks user for inputs (2 pts), doesn’t use ord/chr (3 pts), circles the cipher (6 pts), converts each character (2 pts), builds the resulting string (2 pts), prints the results (2 pts), and matches the example interaction (3 pts)

  • 5 points - readme.txt file contains reflection (5 pts)


What you should submit

You should submit a single a2.zip file on Moodle. It should contain the following files:

  • readme.txt (reflection)
  • popChange.py (Part 1)
  • testingRandom.py (Part 2)
  • simpleCaesar.py (Part 3a)
  • circularCaesar.py (Part 3b)