This assignment has been closed on October 16, 2024.
You must be authenticated to submit your files

Tutorial 2: Mastermind

G. Pogudin d’après U. Fahrenberg

Objectives

What we will practice today: Operations on lists, tuples iteration with for and while

Setup: before you start

  1. Launch Spyder (from the Applications/Programming menu). If you see a “Spyder update” message, just click OK and ignore it.
  2. Create a new project, called Tutorial_2 (using “New Project” from the “Projects” menu). This will ensure that files from last week (and future weeks) do not get mixed up.

Exercises

The aim of this tutorial is to implement the code-breaking game Mastermind. In this game, one player (the codemaker) chooses a secret code consisting of four pins of different colors, and the other player (the codebreaker) has to guess the pattern through several rounds, each time cleverly using the feedback she has received from the codemaker.

We will explain the game in detail below, but it also has a Wikipedia page.

Create a new program file called mastermind.py. Make sure you get the filename right, or else the testing code won’t work. We will be using the random module in this tutorial, so you must add the line

import random

near the top of your mastermind.py file.

Exercise 1: Loops

Mastermind is played using ‘codes’. A code is a list of four elements (“colors”) which each are chosen among the six colors 'RED', 'GREEN', 'BLUE', 'PURPLE', 'BROWN', and 'YELLOW' (with repetitions).

We will use a global variable for the colors. Add the following line to your file mastermind.py:

COLORS = ['RED', 'GREEN', 'BLUE', 'PURPLE', 'BROWN', 'YELLOW']

Warning: we use the American spelling “colors”, and not the British “colours”, throughout this tutorial. Be careful to follow this convention: if you try to use COLOURS, then Python won’t know what this is!

Solving the exercises on this sheet requires using loops in Python. Thus, we will now create a function that uses a for loop to check if the given color exists in our list of COLORS.

Here’s a start:

def input_color(color):
    """Return True if the given input color can be found in COLORS."""
    # Use a for-loop to iterate over the list of COLORS.
    for i in range(6):
        pass # remove this line and replace with your own code

You might prefer to iterate directly over the COLORS list, rather than using indices!

Testing

Test your code in the console, after running mastermind.py. Make sure you can reproduce the following examples:

In [1]: input_color('BROWN')
Out[1]: True

In [2]: input_color('WHITE')
Out[2]: False

In [3]: input_color('CUCUMBER')
Out[3]: False

Now you can upload your solution using the form below, for further automatic testing.

Upload form is only available when connected

Exercise 2: Create a random code

We will be playing our game against the computer, so the first thing we need is a function which can create a random code.

Now define a new function create_code (in mastermind.py) which takes no arguments and returns a 4-element list of strings randomly chosen among the six colors above.

Here’s a start:

def create_code():
    """Return 4-element list of strings randomly chosen from
    COLORS with repetition.
    """
    pass # remove this line and replace with your own code

Hints:

  • Remember to import random.
  • if s is a list, then random.choice(s) returns a random element of s.
  • if s is a list, then command s.append(a) will add element a as the last element of the list.

Testing

Test your code in the console, after running mastermind.py. Here’s what your output should look like (but of course, it’s random, so it will not exactly look like this):

In [4]: create_code()
Out[4]: ['RED', 'RED', 'GREEN', 'BROWN']

In [5]: create_code()
Out[5]: ['PURPLE', 'YELLOW', 'PURPLE', 'BLUE']

Upload your solution using the form below for further automatic testing.

Upload form is only available when connected

Exercise 3: Guess a code

Next we implement functions which allow us to guess a code and receive feedback from the codemaker. The feedback is given as follows:

  • For each pin of the right color in the correct position, the codemaker gives a 'BLACK' pin.
  • For each pin of a right color, but in the wrong position, we receive a 'WHITE' pin.

For example, if the code is ['RED', 'RED', 'GREEN', 'YELLOW'] and we guess ['RED', 'YELLOW', 'YELLOW', 'BROWN'], then we receive one black pin (for guessing the first 'RED') and one white pin (for guessing the 'YELLOW' in the wrong position; note that the second YELLOW guess doesn’t score an extra pin, as there is only one YELLOW pin in the code).

We will first take care of the black pins. Define a new function black_pins (in mastermind.py) which

  • takes two arguments, a guess and a code; and
  • the number of black pins that we receive for the guess.

That is, we expect the following behavior:

In [6]: black_pins(['RED', 'BLUE', 'YELLOW', 'BROWN'], ['RED', 'RED', 'GREEN', 'YELLOW'])
Out[6]: 1

In [7]: black_pins(['RED', 'BLUE', 'BROWN', 'YELLOW'], ['RED', 'RED', 'GREEN', 'YELLOW'])
Out[7]: 2

Here is the start of your function:

def black_pins(guess, code):
    """guess, code: 4-element lists of strings from COLORS
    Returns the number of black pins, determined by the standard
    Mastermind rules
    """
    pass # remove this line and replace with your own code

Testing

Test your code in the console, after running mastermind.py. Start by reproducing the examples above (exactly). Now think of some other tests to make sure your code is correct.

Finally, upload your solution using the form below, and it will be tested automatically.

Upload form is only available when connected

Exercise 4: Guess a code, suite

Now we write a function to determine the number of all pins (black and white) and use it (together with the function from the previous exercise) to find the number of white pins. Remember that according to the Mastermind rules, we receive a pin for every guess of a right color but maybe in a wrong position. That is, we have to compare the modified lists as unordered lists.

Define a new function score_guess (in mastermind.py) that

  • takes two arguments, a guess and a code;
  • runs black_pins to determine the number of black pins;
  • computes the number of all pins, that is the number of colors we guessed correctly (regardless of their position), for example, as follows:
    • for each color, you can find the number of correct guesses of the color as the minimum of the number of occurences of the color in the code and in the guess;
    • the total number of pins will be the sum of these minimums over all colors;
    • substract the number of black pins from the total number of pins to get the number of white pins
  • and then returns a tuple (black, white) containing the number of BLACK and WHITE pins in the feedback.

Here’s a start:

def score_guess(guess, code):
    """guess, code: 4-element lists of strings
    Return (black, white) where
    black is the number of black pins (exact matches), and
    white is the number of white pins (correct colors in wrong places)
    """
    black = black_pins(guess, code)
    # remove this line and complete the function with your own code

Here’s what we expect to happen in the console, once you’ve run mastermind.py:

In [8]: score_guess(['RED', 'GREEN', 'YELLOW', 'BLUE'], ['RED', 'YELLOW', 'BROWN', 'RED'])
Out[8]: (1, 1)

In [9]: score_guess(['RED', 'GREEN', 'YELLOW', 'YELLOW'], ['RED', 'YELLOW', 'BROWN', 'RED'])
Out[9]: (1, 1)

In [10]: score_guess(['RED', 'RED', 'RED', 'YELLOW'], ['RED', 'YELLOW', 'BROWN', 'RED'])
Out[10]: (1, 2)

In [11]: score_guess(['RED', 'YELLOW', 'BROWN', 'RED'], ['RED', 'RED', 'RED', 'YELLOW'])
Out[11]: (1, 2)

Hints:

  • There is a built-in function count in Python, see the docs.;
  • Example run:
    • let’s say the function is called by: score_guess(['RED', 'RED', 'GREEN', 'YELLOW'], ['RED', 'GREEN', 'YELLOW', 'BROWN']);
    • then, we first run black_pins(['RED', 'RED', 'GREEN', 'YELLOW'], ['RED', 'GREEN', 'YELLOW', 'BROWN']) which will return 1;
    • for each color in COLORS, count occurences in guess and code: for RED we get 2 and 1, for GREEN 1 and 1, for BLUE 0 and 0, for PURPLE 0 and 0, for BROWN 0 and 1, and for YELLOW 1 and 1;
    • for each COLOR, we take the minimal value, thus we get 1, 1, 0, 0, 0 and 1 and take the sum of those which is 3;
    • three is the total number of pins that will be shown, however, as we have already calculated that we return 1 black pin, the number of white pins is 2.

Testing

Now test your solution. Start by trying the four examples above, and make sure that you reproduce the output exactly. Then, think of some new tests of your own.

Finally, upload your solution to the form below for further automatic testing.

Upload form is only available when connected

Exercise 5: User input

We will also need a function which lets the user input a guess.

Define a function input_guess which

  • prints Enter your guess: and lets the user input four colors from COLORS,
  • prompting the user first for a 1st color:, then a 2nd color: etc.,
  • and returns the guess as a list of four colors.

Your function needs to make sure that the user inputs colors which are contained in COLORS. If the user enters a color that is not in COLORS, then your program should ask them to try again.

Complete the following function, adding it to your program mastermind.py:

def input_guess():
    """Input four colors from COLORS and return as list."""
    pass # remove this line and replace with your code

Hints:

  • Hit ctrl-C in the console if your code “hangs” on an input.
  • Use the str_with_suffix function from last week to convert numbers to ordinal strings.
  • Use your function input_color to check if the entered color is correct.

Testing

Test your code in the console, after running mastermind.py:

In [12]: input_guess()
Enter your guess:

1st color: RED

2nd color: YELLOW

3rd color: MAUVE
Please input a color from the list ['RED', 'GREEN', 'BLUE', 'PURPLE', 'BROWN', 'YELLOW']

3rd color: BROWN

4th color: RED
Out[12]: ['RED', 'YELLOW', 'BROWN', 'RED']

Upload your solution to the form below, it will be tested automatically.

Upload form is only available when connected

Exercise 6: Playing one round of the game

We are now ready to implement one round of the Mastermind game.

Define a function one_round (in mastermind.py) which

  • uses input_guess to request input from the user,
  • uses score_guess to determine the number of black and white pins to award,
  • prints the number of black and white pins,
  • and returns True if the user has won, and False otherwise.

Complete the following function, adding it to your program mastermind.py:

def one_round(code):
    """Input guess, score guess, print result, and return True iff
    user has won.
    """
    pass # remove this line and replace with your code

Testing

Test your code in the console, after running mastermind.py. First, make sure you can reproduce the following examples exactly:

In [13]: one_round(['YELLOW', 'RED', 'BROWN', 'RED'])
Enter your guess:

1st color: RED

2nd color: RED

3rd color: RED

4th color: BLUE
Score: 1 black, 1 white
Out[13]: False

In [14]: one_round(['YELLOW', 'RED', 'BROWN', 'RED'])
Enter your guess:

1st color: YELLOW

2nd color: RED

3rd color: BROWN

4th color: RED
Score: 4 black, 0 white
Out[14]: True

Now see if you can think of some more tests to catch possible bugs.

Finally, upload your solution using the form below for further automatic testing.

Upload form is only available when connected

Exercise 7: Mastermind!

Now we can finally bind things together to play Mastermind against the computer.

Define a function play_mastermind (in mastermind.py) that

  • takes a code as input,
  • lets the user play in rounds, keeping track of how many rounds have been played,
  • until the user has won, in which case it prints You win!

Here’s a start:

def play_mastermind(code):
    """Let user guess the code in rounds."""
    # Hint: use a while loop!
    pass # remove this line and replace with your own code

Here’s what we expect to happen in the console, once you’ve run your code:

In [15]: play_mastermind(['PURPLE', 'GREEN', 'YELLOW', 'BLUE'])

Round 1
Enter your guess:

1st color: PURPLE

2nd color: BLUE

3rd color: YELLOW

4th color: BROWN
Score: 2 black 1 white

Round 2
Enter your guess:

1st color: PURPLE

2nd color: GREEN

3rd color: YELLOW

4th color: BLUE
Score: 4 black 0 white
You win!

Testing

Test your code using the examples above in the console, after running mastermind.py. Make sure you can reproduce them exactly! Can you think of any more useful tests!

Finally, upload your solution using the form below, it will be tested automatically.

Upload form is only available when connected

Extension Exercise 8: Making things more user-friendly

As you may have noticed, entering color guesses as the strings YELLOW, GREEN, etc. is rather tiresome. We would like to make our Mastermind program more user-friendly by accepting inputs which do not conform to our specification but where it is clear what the user meant.

To be precise, we want to accept unambiguous prefixes of color names: there is only one color starting with Y, so we will accept Y, YE, YEL, YELL, and YELLO, all to stand for YELLOW. On the other hand, we cannot accept B, since this is the start of both BLUE and BROWN; but BL and BR would be unambiguous.

Copy your mastermind.py into a new file, mastermind_plus.py; this is where you will program your extensions to Mastermind.

Switching to mastermind_plus.py, change your input_guess function so that it accepts unambiguous prefixes of color names. Any accepted input must be converted to the corresponding color names before being returned.

Here’s what we expect to happen in the console, once you’ve run your code:

In [16]: input_guess()
Enter your guess:

1st color: Y

2nd color: B
Please input an unambiguous prefix of a color from the list
['RED', 'GREEN', 'BLUE', 'PURPLE', 'BROWN', 'YELLOW']

2nd color: BR

3rd color: BLU

4th color: R
Out[16]: ['YELLOW', 'BROWN', 'BLUE', 'RED']

Hints:

  • Python string objects have a useful method called startswith. If full and prefix are strings, then full.startswith(prefix) will evaluate to True if full starts with the substring prefix, and False otherwise.

Testing

Now test your code in the console, after running mastermind_plus.py.

If you use create_code instead of giving a code as input, you can now play against the computer. How many rounds does it take you to win? (It has been shown that 5 rounds are enough when you play optimally!)

Upload your solution to the form below.

Upload form is only available when connected

Extension Exercise 9: Generalized Mastermind

There is nothing special about the six colors 'RED', 'GREEN', 'BLUE', 'PURPLE', 'BROWN', and 'YELLOW' we have chosen for our Mastermind implementation; any other set of six different items will do. In fact, also the number six here is somewhat arbitrary; there exist versions of Mastermind with five colors or eight. Finally, the fact that our implementation uses codes consisting of four elements is also arbitrary, and there are again versions of Mastermind which use codes of three, five or six elements.

Just to stress the point, Disney published a version of Mastermind in the 70s which uses codes of three out of five Disney characters (here are some pictures).

The difficulty of solving the exercises below will depend on the way you have implemented your solutions to the mandatory exercises; the easier it is to generalize things, the better your code quality.

  1. Change your code so that you can replace COLORS list with any list (try six Norse Gods 'Odin', 'Thor', 'Freya', 'Skadi', 546 'Frigg', and 'Heimdall', then try to add 'Loki').

  2. Change your Mastermind so that codes (and guesses) are of length equal to global constans CODE_LENGTH instead of four. Check that everything still works as expected.

Testing

It is important to test your code.

Upload your solution in mastermind_plus.py using the form below.

Upload form is only available when connected

Difficult Extension Exercise 10: Pitting the computer against itself!

Playing Mastermind against the computer quickly gets boring, so let’s see if we can program the computer to play against itself! (Before going further, revert any changes from the previous exercise to go back to the original Mastermind version.)

There’s an algorithm for solving Mastermind, first described by Don Knuth, which works as follows:

  1. Try a guess x, x, y, y, where x and y are two different colors.

  2. Find the set \(S\) of all possible codes: all codes which would get you the score which your current guess did.

  3. For each possible guess \(g\) and each black-white answer pin combination \(a\), find the number of guesses which would be eliminated from \(S\) if we used \(g\) as next guess and had \(a\) as answer. The score of \(g\) is the minimum of these numbers over all possible answer-pin combinations.

  4. Take one of the \(g\) with maximum score and use as next guess.

Knuth has shown that this algorithm always reaches a solution after at most five steps. Define a function autoplay_mastermind which implements Knuth’s algorithm.

Hints:

  • imagine that the wanted code is one of the possible permutations on 4 elements of your colors. Steps 2, 3 and 4 try to find a consistent set of possible solutions by comparing guesses against each other and counting the resulting scores.

  • step three can also be described in the following way (found here: https://stackoverflow.com/questions/62430071/donald-knuth-algorithm-mastermind):

“Take the set of untried codes, and call it \(T\).

Iterate over \(T\), considering each code as a guess, \(g\).

For each \(g\), iterate over \(T\) again considering each code as a possible true hidden code, \(c\).

Calculate the black-white peg score produced by guessing \(g\) if the real code is \(c\). Call it \(s\).

Keep a little table of possible scores, and as you iterate over the possible \(c\), keep track of how many codes produce each score. That is, how many choices of \(c\) produce two-blacks-one-white, how many produce two-blacks-two-whites, and so on.

When you’ve considered all possible codes (for that \(g\)) consider the score that came up the most often. You might call that the least informative possible result of guessing \(g\). That is \(g\)’s score; the lower it is, the better.

As you iterate over \(g\), keep track of the guess with the lowest score. That’s the guess to make.”

Upload your solution using the form below (no automatic tests for this exercise though).

Upload form is only available when connected