Details

Due date

This assignment is due on Thursday, April 18th, at 11:30PM

Coverage

This HW is the first assignment in the form of a Mini Project (MP). The rest of HW assignments will be Mini Projects, where you will practice what you've learned in lectures, readings, and Tuesday labs to implement a working program, ranging from games, biology, physics, etc.

This Mini Project focuses on program decomposition with functions, user input with input, strings, lists, loops, and documentation with docstrings all used collectively to implement an interactive game of Mastermind.

What to hand in

You will be handing in the following files for this Mini Project on CodePost as usual:

  • mp2_mastermind.py.

  • my_pytest_mp2.py.

Do not hand in any of the test scripts we provide (test_mp2.py).

In order for your files to work with the test scripts, make sure that there is no code except for the functions (and docstrings) that we ask for. In particular, there should be no top-level code that is outside a function, because if there is then it will run when the tests run, which will make the output very hard to understand. Remember that you can always import your code from the Python interpreter and run it there!

The following is the expected structure for your program, which you can download as starter code here (again, note that there are no variables outside of your function definitions):

"""
Student name:
Date

Brief program overview (2-3 sentences)

<Additional reminders that should be removed>
"""

import random

# Function definitions each with valid """...""" docstrings, separated by two lines each

if __name__ == '__main__':
    run_game()

Grading notes

Similar to HW1, this assignment is graded out of 30 points which include functional correctness, documentation with docstrings for your file and each function, limiting variables only to those in your functions and __main__ branch, following Python 3 conventions taught in lecture and the CS 1 Code Quality Guide, and overall adherence to the spec.

For example, if your program outputs any input prompts or print statements differently than specified here, you will be ineligible for full credit.

As described in the syllabus, your resulting MP2 grade will be out of 3 points, rounding (your score out of 30 / 10) to the nearest integer.You should not be implementing any behavior that is not specified.

Reminder: Mini Projects/HWs are strictly no collaboration. You are not allowed to look at other students' code, nor online solutions. Submissions which are flagged for suspicion of violating the CS 1 or Caltech Honor Code will be escalated to the BoC. We have many resources to help you succeed in this class, and you are encouraged to utilize them if you get stuck!

Documentation

From this point on, we expect you to write docstrings for all of your functions. You can write short (one-line) docstrings for very simple functions, but for most functions we expect you to write multiline docstrings explaining what the function does (but not how it does it!), what the arguments mean, and what the return value (if any) means. Also, if the function has some other kind of effect (like reading from a file or printing to the terminal), mention that too. You don’t have to write a long description, but try to write enough so that someone who didn’t know what the function did could use it correctly (similar to one using the help function you practiced in HW1). If you’re unclear on whether your docstrings are good enough, ask the TAs or El during office hours.

Write the docstring prior to writing the function. Doing this can clarify your understanding of what the function is supposed to do.

Using pytest for testing

For this assignment (and all future assignments), we will be using the pytest testing framework. This package provides the pytest program as well as Python modules which will make it much easier to test the code that you write. In particular, this will make it easier for you (and your TA!) to figure out if your code is correct or not.

Installation

To install the pytest framework, open up a terminal and type this command:

$ pip3 install pytest

(The $ is just the prompt; don’t type that.)

In case you’re wondering, pip3 is the Python 3 package manager. Many wonderful things can be installed using pip3. [1]

To check that the installation succeeded, start up the Python interpreter and type:

>>> import pytest

If it gives you the prompt again without reporting any errors, you’ve installed pytest. If not, ask a TA for help or ask a question on Discord.

Most of the time, we will be using the pytest program directly from the command-line. To make sure that this program is available, type this:

$ pytest --help

If you see a long help message typed to your terminal, the program has been installed. If you see an error message, first try the following (you may have run into this requirement of adding python3 -m when using pycodestyle as well in Lab 01), otherwise ask a TA for help or ask a question on Discord.

>>> python3 -m pytest --help

If this fixes the error, you'll need to add python3 -m before pytest program_name.py when testing your programs.

Assuming that pytest is installed, you can check which version is running by typing:

$ pytest --version

The most current version is version 6.2.5.

Documentation

The pytest documentation is here, but you don’t need to read it (it may be more confusing than helpful to you at this stage). We will only be using the most basic features of pytest for the next few Tuesday Labs/Mini Projects.

Basic use

The most basic way to use pytest is simply to have a Python module whose name starts with test_. (So you might have test_mycode.py, for instance.) Inside this module (which is also known as a test script) you put functions whose names start with test_. For instance, you might have:

# These functions would normally be defined in another module and imported here.
def double(x):
    return x * 2

def triple(x):
    return x * 3

def quadruple(x):
    return x * 4

# These are the test functions.

def test_double():
    assert double(42) == 84

def test_triple():
    assert triple(42) == 126

def test_quadruple():
    assert quadruple(42) == 168

Let’s say you saved this as test_mycode.py. Then you could run it by typing this at the terminal prompt:

$ pytest test_mycode.py

or

$ python3 -m pytest test_mycode.py

You should see output like this:

============================= test session starts ==============================
platform darwin -- Python 3.9.0, pytest-6.1.1, py-1.9.0, pluggy-0.13.1
rootdir: /Users/mvanier/_/src/MCV/CS1-2020-prep/mike/assignments/2
collected 3 items

test_mycode.py ...                                                       [100%]

============================== 3 passed in 0.01s ===============================

The collected 3 items line means that there are three tests in the file. After the test_mycode.py you see three dots, which means that all of the tests were executed and passed.

Let’s intentionally make a test fail. Change the last line of test_mycode.py to:

    assert quadruple(42) == 0

and rerun pytest test_mycode.py. Here is the result:

============================= test session starts ==============================
platform darwin -- Python 3.9.0, pytest-6.1.1, py-1.9.0, pluggy-0.13.1
rootdir: /Users/student/code
collected 3 items

test_mycode.py ..F                                                       [100%]

=================================== FAILURES ===================================
________________________________ test_quadruple ________________________________

    def test_quadruple():
>       assert quadruple(42) == 0
E       assert 168 == 0
E        +  where 168 = quadruple(42)

test_mycode.py:20: AssertionError
=========================== short test summary info ============================
FAILED test_mycode.py::test_quadruple - assert 168 == 0
========================= 1 failed, 2 passed in 0.03s ==========================

This gives you a description of exactly what went wrong.

Assertions

We haven’t shown you what the assert statement does in Python yet. In an assert statement, the assert keyword is followed by an expression which should evaluate to True or False. If it evaluates to True, nothing happens; the program just keeps on executing. If it evaluates to False, an exception is raised. Assertions are normally used as a check that something is true at a given point in the execution of a program. Here, we are using them to check that functions behave in an expected way; if they don’t, then pytest can detect that and keep track of which functions ran correctly and which didn’t.

There is a lot more that you can do with pytest. We will cover these extra features when we need them.

MP2 Test Suite

Once you have written your mp2_mastermind.py file, you can test it by running a "test suite", which is a test file that we have prepared for you. The test suite is called test_mp2.py and can be downloaded here. Download it to the same directory as your mp2_mastermind.py file.

To run the test suite, open up a terminal, go to the directory containing your code, and type this at the terminal prompt:

$ pytest test_mp2.py

If all is well, it will print some green dots and not report any failures. If there is a test failure, it will report that and indicate which function failed. If that happens, you have some bugs to fix! Note, however, that even if the test script doesn’t report any errors, it doesn’t guarantee that there are none. Test scripts almost never cover 100% of the possible cases, and this one certainly doesn’t. Nevertheless, if your code passes the tests you can at least be more confident that it’s correct.

Coding tips

When writing your functions, try to come up with a simple solution if at all possible. If you write an extremely convoluted/complex solution where a simple solution will work just as well, we will probably take marks off even if your function works correctly. (This is not to say that there is only one solution for these problems, only that we don’t want unnecessary complexity.)

This applies to all parts of this project and all future assignments as well.

Overview: The game of Mastermind

Mastermind is a simple board game for two players. The rules are listed here, but here is a quick summary. One player (the "codemaker") picks a secret code consisting of four code pegs which can be any of six colors (we’ll use the colors red, green, blue, purple, orange, and white, which we’ll abbreviate as 'R', 'G', 'B', 'P', 'O', and 'W'). The order of the pegs is important, so a code of 'RGBB' is not the same as a code of 'BRGB'. Colors can be duplicated in a code, but they don’t have to be. The codemaker puts the four pegs representing their code in a secret location where the other player can’t see them. That player (the "codebreaker") tries to guess the code by laying down four code pegs that they think might be the correct code. The codemaker compares the guess with the real code, and puts down zero to four key pegs which indicate how close the guess is to the solution. For every code peg which is the correct color in the correct location, a black peg is put down. For every code peg which is a correct color but not in a correct location, a white peg is put down. Victory occurs when four black pegs are put down, which means that all the code pegs in the guess are the same as those in the secret code. The objective is to guess the code in as few moves as possible.

Here’s an example of a sample game. Let’s say that the secret code was 'ROOB' (Red, Orange, Orange, Blue). We’ll show the code pegs on the left and the key pegs on the right (as black ('b'), white ('w'), or blank ('-')).

Guess    Result
-----    ------

RGBP     bw--   (R is in the correct location, B is not, other colors wrong)
OOWW     bw--   (One O is in the right location, one isn't, other colors wrong)
RBOR     bbw-
RBBW     bw--
RBOO     bbww   (All colors correct, but two are not in the right locations)
ROOB     bbbb   (All colors correct and in the right order; victory!)

The order of the key pegs is unimportant, so 'b---', for instance, doesn’t necessarily mean that the first code peg is in the correct location. For convenience, we put the black key pegs first, then the white ones, then the dashes.

Your job will be to write a computer program to play Mastermind against you. The computer will be the codemaker and will choose a random code. You will be the codebreaker. The computer has to take your guesses and report how well you are doing. An interaction with the computer will look like this:

New game.
Enter your guess: RGBW
    Result: bw--
Enter your guess: OOWW
    Result: bw--
Enter your guess: RBOR
    Result: bbw-
Enter your guess: RBBW
    Result: bw--
Enter your guess: RBOO
    Result: bbww
Enter your guess: ROOB
    Result: bbbb
Congratulations!  You cracked the code in 6 moves!

We will build up to this in stages. All of the code for this miniproject should be in a file called mp2_mastermind.py. Don’t forget to write docstrings!

None of these functions need to be very long, so if you find yourself writing a lot of code for any function (say, more than 20 non-docstring lines), you are probably going about the task the wrong way and need to speak to a TA.

0. Designing your own tests

[25]

Before starting the implementation of Mastermind, we ask that you go over the descriptions of make_random_code, count_exact_matches, count_letter_matches, and compare_codes and write tests for those functions. You will make your own pytest tests in a seperate file that you will call my_pytest_mp2.py and submit on Codepost along with mp2_mastermind.py. Thinking about the desired outputs will help you in designing your functions and save you time when writing code. We recommend that you take a look at the tests we've provided you as an example. Your file should contain the following:

  • test_make_random_code() with at least 5 different test cases (Update: We are no longer requiring this since it's a bit trickier to test for randomness, but you can try for an added challenge! Hint: How could you use a loop to test "enough" times that your function eventually generates all possible characters?)

  • test_count_exact_matches() with at least 5 different test cases.

  • test_count_letter_matches() with at least 5 different test cases.

  • test_compare_codes() with at least 5 different test cases.

You are not expected to have more than a simple assert statement for each different test case but make sure to cover a wide array of cases. Ask yourself, what are some edge cases that you may not have considered. Please do not copy the same test cases that we have provided. Finally, once you have finished the assignment, you should make sure to run your file using pytest to make sure that your 4 tests pass!

1. make_random_code

[15]

Write a function called make_random_code which takes no arguments and returns a string of exactly four characters, each of which should be one of 'R', 'G', 'B', 'P', 'O', or 'W'. You may find the random.choice function very useful for this problem. Do:

>>> help(random.choice)

from inside Python to learn more about it. (Make sure you import the random module first!)

2. count_exact_matches

[15]

Write a function called count_exact_matches which takes two arguments which are both strings of length 4 (you don’t have to check this). It returns the number of places where the two strings have the exact same letters at the exact same locations. So if the two strings are identical, it would return 4; if the first and third letters of both strings are the same but the other two are different, it would return 2; and if none of the letters are the same at corresponding locations it would return 0.

>>> count_exact_matches('RGBP', 'RGBP')
4
>>> count_exact_matches('RGBP', 'GBPR')   # letters are not in the same locations
0
>>> count_exact_matches('RRRR', 'OOOO')
0
>>> count_exact_matches('RORP', 'RPOR')   # only one letter is in the same location
1
>>> count_exact_matches('RGRP', 'RGRR')   # only one letter is _not_ in the same location
3
>>> count_exact_matches('RGBP', 'RGOO')
2
>>> count_exact_matches('RGBP', 'RWBW')
2
>>> count_exact_matches('RGBP', 'WOWO')
0

Use a for loop, which should iterate over the indices of the string (i.e. the list [0, 1, 2, 3]). Recall that you can access letters of a string using the same syntax you use to access elements of a list (because they’re both sequences).

3. count_letter_matches

[30]

Write a function called count_letter_matches which is like count_exact_matches except that it doesn’t care about what order the letters are in; it returns the number of letters of the two strings that are the same regardless of order.

>>> count_letter_matches('RGBP', 'RGBP')
4
>>> count_letter_matches('RRRR', 'RRRR')
4
>>> count_letter_matches('RRRR', 'ROOO')
1
>>> count_letter_matches('RGBP', 'WOWO')
0
>>> count_letter_matches('RGBP', 'GBPR')  # N.B. the order doesn't matter
4
>>> count_letter_matches('RORO', 'OROR')
4
>>> count_letter_matches('ROGO', 'RRGG')
2
>>> count_letter_matches('RGBP', 'OROG')
2
>>> count_letter_matches('RGBP', 'BWBR')
2
>>> count_letter_matches('RBBP', 'GRBO')
2

This function is a bit tricky, so here are some hints. Of course, you don’t have to follow our hints; you can solve this problem any way you like as long as the solution isn’t ridiculously long or convoluted.

  • Convert one of the two strings into a list using the list built-in function before doing anything else (think about why only one is needed!).

  • Go through one sequence (remember that strings, lists, and range are all Python sequences) character by character using a for loop. For each character that is in the other sequence, increment a counter variable and then remove the character from the list (not the sequence you are iterating over) using the remove method on lists. Use the form <x> in <seq> (see section A) to find out if a character is in a sequence.

  • Once you have gone through, the counter variable is the answer.

4. compare_codes

[30]

Write a function called compare_codes which takes two arguments which are both strings of length 4. The first argument is called code and will represent the secret code chosen by the codemaker. The second argument is called guess and will represent the guess of the codebreaker. The function will output a string of length four consisting only of the characters 'b', 'w', and '-' (for black, white, and blank key pegs). Note that the '-' character must be the hyphen/dash character, not the '_' underline character. This 4-character string represents the key pegs i.e. the evaluation of the guess (so 'bbbb' would be a perfect guess, 'wwww' would mean all colors are correct but in the wrong order, and '----' would mean that no colors are correct).

This function is the heart of the game. Fortunately, if you’ve written the two previous functions correctly, writing this one is a piece of cake. Here is the algorithm (solution method) you should use:

  • The count of black pegs can be obtained just by calling the function count_exact_matches.

  • The count of white pegs can be obtained by calling both the functions count_letter_matches and count_exact_matches and subtracting the result of the second function call from the first. (Note that since you’ve already called count_exact_matches on the two strings, you shouldn’t call it a second time.)

  • The count of blank pegs is the difference between 4 and the sum of the count of black and white pegs.

You will need to take these three counts and create a string for the result. Make sure that the string includes the 'b's first, then the 'w's, then the '-'s.

Note that this function doesn’t print anything; it just returns a string.

>>> compare_codes('RGBP', 'RGBP')
'bbbb'
>>> compare_codes('RGBP', 'WOWO')
'----'
>>> compare_codes('RGBP', 'PBGR')
'wwww'
>>> compare_codes('ROBO', 'RWBW')
'bb--'
>>> compare_codes('RBGP', 'RGBP')
'bbww'

5. run_game

[30]

Now we’re ready to write the function that actually runs the game. It will be called run_game, and it will take no arguments. When called, this function will do the following:

  • Print New game..

  • Select a secret code using the make_random_code function you defined above, and store this in a variable.

  • Prompt the user for a guess using the input function and the prompt string Enter your guess: .

  • Evaluate how well the guess matches the stored code using the compare_codes function and print out the result in the format Result: <result string>.

  • If the result string is bbbb, then the function will print Congratulations! You cracked the code in <N> moves! and exit (where <N> is the number of moves since the game began). Otherwise, the function will go back to step 3 above.

In other words, the interaction with the user should be the same as what was described above at the beginning of this section. Here are a few hints:

  • Every time the function asks for a guess, a counter which holds the number of moves should be incremented.

  • Consider using an infinite loop (with a while statement) and a break statement inside the loop once the exit condition applies.

  • Make sure your output and input prompts match that as described; make sure to test your program!

  • While testing, we recommend fixing 1-2 randomly-generated codes when you run the program to see if you get the expected output/guess counts. Try starting with the code of 'ROOB' following the interaction shown above to check that you get the expected results until a game is one, then try a few other test cases (e.g. 'RORO', 'RRRR', 'ROPB'). Make sure to change this fixed string to the result of make_random_code before submitting!

6. Using the debugger

[30]

For this part of the assignment, you will use the debugger to step through the execution of your Mastermind game code. You will focus on the interaction between compare_codes and its helper functions, count_exact_matches and count_letter_matches. This will help you understand how these functions work together to evaluate a guess in the game. If you are not familiar enough with the debugger, you can take a look at the walkthrough guide posted on the website under resourses. Then:

  • Place a breakpoint at the beginning of compare_codes.

  • Place additional breakpoints at the beginning of the count_exact_matches and count_letter_matches.

  • Start the debugger session by executing your Mastermind game code.

  • Use the "Step Into" feature to move through the code line by line.

Pay special attention to the how the execution moves from function to function and how the data is passed into them. Note how the variables change and the conditions under which helper functions are called and how their return values affect the execution of compare_code. Consider how understanding the flow between functions and their helpers can help in debugging and how you might use this strategy in the future to diagnose and fix bugs. In addition to your code, submit 1 to 2 paragraphs in a docstring at the bottom of MP2_mastermind.py on your observations and reflection. You should explain the flow between the different functions we highlighted when running Mastermind. Make sure to include any questions you may have.

Final Reminders

Here are a few final reminders to check before submitting your assignment:

  • Make sure you are running the provided test_mp2.py test suite as described above. For full credit, your program should not produce any errors. If any errors are reported, you have some debugging to do.
  • Double-check your code for proper docstrings following the material in Lecture 3 and the CS 1 Code Quality Guide.
  • Make sure you are following Python language conventions taught in class/the CS 1 Code Quality Guide. Some common conventions students have misused in the past for this assignment include:
    • Not using lowercase_snake_casing for function and variable names (no upper-case characters!)
    • Make sure you are using spaces, not tabs (refer to Lecture 3 video where El answered a student question/showed how to configure VSCode settings if needed)
    • Make sure you don't have any unused/unnecessary variables in functions
    • No variables should be outside the scope of a function
    • When using loops, make sure you are not using them unnecessarily
    • Do not use any advanced Python features that we have not taught; the reason for this requirement is 1. students are not expected to have prior experience and 2. students who do have some experience have often misused advanced Python without understanding trade-offs where simpler implementations are more appropriate. Advanced features include list comprehension, any modules that are not random, functional Python (e.g. map), etc. If you want to discuss these trade-offs/ask about exceptions, you should reach out to El (not TAs); they are happy to talk about this!
  • And finally, double-check this spec once more to make sure you've followed any missed requirements!

1. You can probably get away with just typing pip install pytest, but pip3 is safer in case you have Python 2 installed on your computer alongside Python 3. pip3 will only add packages to Python 3. If for some reason pip3 doesn’t work, try just pip, and if neither works, ask a TA for help.