Mastermind¶
import random
In this notebook, you will write a game called Mastermind. Play the video below to learn about the rule of the game.
%%html
<iframe width="800" height="415" src="https://www.youtube.com/embed/wsYPsrzCKiA" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
Mastermind first creates a hidden
codeof lengthcode_lengthconsisting code pegs with possibly duplicate colors chosen from a sequence ofcolors.Coderbreaker provides a
guessof thecode.Mastermind generates a
feedbackconsisting of key pegs of black and white colors:The number of black pegs (
black_key_pegs_count) is the number of code pegs that are the correct colors in the correct positions.The number of white pegs (
white_key_pegs_count) is the number of code pegs that are the correct colors but in incorrect positions.Each code peg should be counted only once, i.e., a code peg cannot be awarded more than one key peg. E.g.,
If the
codeis'RBGG'andguessis'BGGG', thenthe feedback should be
'bbw'withblack_key_pegs_count == 2because of__GGin the guess, andwhite_key_pegs_count == 1because of_B__in the guess._G__in theguessshould not be awarded an additional white peg because__GGin thecodehas been counted.
Codebreaker wins if the code is correctly guessed within a certain number (
max_num_guesses) of guesses.
Random Code Generation¶
The first exercise is to generate a random hidden code so even one person can play the game as Codebreaker. Watch the following video to understand how computers generate random numbers.
%%html
<iframe width="800" height="450" src="https://www.youtube.com/embed/GtOt7EBNEwQ" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
To generate random content in Python, we can use the random module imported at the beginning of the notebook. The module provides some useful functions to generate random objects as follows.
for i in range(10):
print(random.random()) # random floating point numbers in [0,1)
0.7827968849417102
0.7912710422467361
0.18580256638087056
0.15141340652226887
0.4816758500421424
0.1835795997968771
0.60719381222744
0.2645502008598918
0.1308987733962963
0.6784929837440095
for i in range(10):
print(random.randint(3, 10), end=" ") # random integer in range [3,10]
10 10 6 5 7 7 4 8 6 9
for i in range(10):
print(random.choice("RBG"), end="") # random element in the sequence 'RBG'
GGBBGGGGRR
We can generate a reproducible pseudo-random sequence by specifying the seed.
# repeatedly run the cell to see new sequences.
random.seed(123456)
for i in range(10):
print(random.randint(3, 10), end=" ")
7 3 5 3 4 3 7 3 4 6
By default random uses the system time as the seed. We can call seed without any argument to revert to the default behavior.
# repeatedly run the cell to see new sequences.
random.seed()
for i in range(10):
print(random.randint(3, 10), end=" ")
4 6 3 10 7 3 3 7 4 4
Exercise Define a function that generates a random code. The functions take in
a string
colorswhose characters represent distinct colors to choose from, anda positive integer
code_lengthrepresenting the length of the code.
For instance, get_code('ROYGBP',4) returns a code of 4 code pegs randomly with colors chosen from
'R'ed,'O'range,'Y'ellow,'G'reen,'B'lue, and'P'urple.
One possible outcome is 'ROYY'.
def get_code(colors, code_length):
code = ''
# YOUR CODE HERE
raise NotImplementedError()
return code
Guess Validation¶
Exercise Define a function valid_code that
takes
colors,code_length, andguessas the first, second, and third arguments respectively, andreturns
Trueifguessis a valid code, i.e., a string of lengthcode_lengthwith characters from those ofcolors, andFalseotherwise.
# YOUR CODE HERE
raise NotImplementedError()
Hint
Solution template:
def ___(colors, code_length, guess):
if len(guess) ___ code_length:
is_valid = ___
else:
for peg in guess:
for color in colors:
if peg == color: ___
else:
is_valid = ___
___
else:
is_valid = ___
return is_valid
# tests
assert valid_code("RBG", 1, "R") == True
assert valid_code("RBG", 2, "B") == False
assert valid_code("RBG", 2, "RP") == False
assert valid_code("RBG", 0, "") == True
Feedback Generation¶
According to the rules of Mastermind, double-counting of a single peg (as black and white) is not allowed. To facilitate this check, we have written a new module markposition that allows you to mark any non-negative integer position as counted.
Exercise Write an import statement to import from the module markposition the functions
mark_as_countedcheck_if_counted, andreset_all_to_not_counted.
# YOUR CODE HERE
raise NotImplementedError()
# tests
reset_all_to_not_counted()
mark_as_counted(3)
assert check_if_counted(3) and not check_if_counted(0)
Exercise Using the functions imported from markposition, mark only the positions 0, 2, 4, 6, 8, and 10 as counted. All other positions are not counted. Use help to learn how to use the imported functions.
# YOUR CODE HERE
raise NotImplementedError()
# tests
for i in range(11):
assert not check_if_counted(i) if i % 2 else check_if_counted(i)
Exercise Define a function get_feedback that
takes
codeandguessas the first and second arguments respectively, andreturns a feedback string that starts with the appropriate number of characters
'b'(for black key pegs) followed by the appropriate number of characters'w'(for white key pegs).
# YOUR CODE HERE
raise NotImplementedError()
Hint
Solution template:
def get_feedback(code, guess):
black_key_pegs_count = white_key_pegs_count = counted = 0
reset_all_to_not_counted()
for i in ___:
if ___:
black_key_pegs_count += 1
mark_as_counted(i)
for i in range(len(guess)):
for j in range(len(code)):
if ___:
white_key_pegs_count += 1
mark_as_counted(j)
break
key = 'b' * black_key_pegs_count + 'w' * white_key_pegs_count
return key
# tests
def test_get_feedback(feedback, code, guess):
feedback_ = get_feedback(code, guess)
correct = feedback == feedback_
if not correct:
print(
f'With code="{code}" and guess="{guess}", feedback should be "{feedback}", not "{feedback_}".'
)
assert correct
test_get_feedback(10 * "b" + "w" * 0, "RGBRGBRGBY", "RGBRGBRGBY")
test_get_feedback(0 * "b" + "w" * 10, "RGBRGBRGBY", "YRGBRGBRGB")
test_get_feedback(8 * "b" + "w" * 0, "RGRGRGRG", "RGRGRGRG")
test_get_feedback(0 * "b" + "w" * 8, "RGRGRGRG", "GRGRGRGR")
test_get_feedback(0 * "b" + "w" * 6, "RRRRGGG", "GGGGRRR")
test_get_feedback(1 * "b" + "w" * 6, "RRRRGGG", "GGGRRRR")
test_get_feedback(5 * "b" + "w" * 2, "RRRRGGG", "RRRGGGR")
test_get_feedback(1 * "b" + "w" * 0, "RRRRGGG", "RYYPPBB")
test_get_feedback(0 * "b" + "w" * 1, "RRRRG", "GBBBB")
test_get_feedback(0 * "b" + "w" * 0, "RRRRG", "YBBBB")
Play the Game¶
After finishing the previous exercises, you can play the game as a code breaker against a random mastermind.
# mastermind
import ipywidgets as widgets
from IPython.display import HTML, display
def main():
"""The main function that runs the mastermind game."""
max_num_guesses = code_length = code = num_guesses_left = None
is_game_ended = True
colors = "ROYGBP"
color_code = {
"R": "#F88,#F00,#800",
"O": "#FD8,#F80,#840",
"Y": "#FF8,#FF0,#AA0",
"G": "#8F8,#0F0,#080",
"B": "#88F,#00F,#008",
"P": "#F8F,#F0F,#808",
"b": "#888,#000,#000",
"w": "#FFF,#EEE,#888",
}
# returns the HTML code for a colored peg.
def getPeg(color, size=30):
return """<div style='display:inline-block;
background-image: radial-gradient(circle, {0});
width:{1}px; height:{1}px; border-radius:50%;'>
</div>""".format(
color_code[color], size
)
colors_display = widgets.HBox(
[widgets.Label(value="Color codes:")]
+ [
widgets.HBox([widgets.Label(value=color), widgets.HTML(getPeg(color))])
for color in colors
]
)
max_num_guesses_input = widgets.IntSlider(
min=5, max=15, value=10, description="# guesses:"
)
code_length_input = widgets.IntSlider(
min=2, max=10, value=4, description="Code length:"
)
code_input = widgets.Password(description="Code:")
start_new_game_button = widgets.Button(description="Start a new game")
guess_input = widgets.Text(description="Guess:")
submit_guess_button = widgets.Button(description="Submit guess")
board = widgets.Output()
message = widgets.Output()
display(
widgets.VBox(
[
max_num_guesses_input,
code_length_input,
colors_display,
widgets.HBox([code_input, start_new_game_button]),
widgets.HBox([guess_input, submit_guess_button]),
board,
message,
]
)
)
# A listener that starts a new game
def start_new_game(button):
nonlocal code, num_guesses_left, is_game_ended, max_num_guesses, code_length
max_num_guesses = max_num_guesses_input.value
code_length = code_length_input.value
board.clear_output()
message.clear_output()
code = code_input.value or get_code(colors, code_length)
with message:
if not valid_code(colors, code_length, code):
display(
HTML(
"""<p>The code {} is invalid.<br>
Leave the code box empty to randomly generated a code.
</p>""".format(
code
)
)
)
is_game_ended = True
else:
num_guesses_left = max_num_guesses
is_game_ended = num_guesses_left <= 0
display(
HTML(
"<p>Game started! {} Guesses left.</p>".format(num_guesses_left)
)
)
# A listener that submits a guess
def submit_guess(button):
nonlocal num_guesses_left, is_game_ended
guess = guess_input.value
with message:
message.clear_output()
if is_game_ended:
display(
HTML(
"""<p>Game has not started.<br>
Please start a new game.</p>"""
)
)
return
if not valid_code(colors, code_length, guess):
display(HTML("<p>Invalid guess.</p>"))
return
feedback = get_feedback(code, guess)
num_guesses_left -= 1
with board:
content = ""
for k in guess:
content += getPeg(k)
content += """<div style='display:inline-block;
margin: 0px 5px 0px 30px;
position:relative; top:5px;'>Feeback:</div>
<div style='display:inline-block;
border: 1px solid; width:120px; height:30px;'>"""
for k in feedback:
content += getPeg(k, 28)
content += "</div>"
display(HTML(content))
with message:
message.clear_output()
if feedback == "b" * code_length:
is_game_ended = True
display(
HTML(
"<p>You won with {} guesses left!</p>".format(num_guesses_left)
)
)
return
is_game_ended = num_guesses_left <= 0
if is_game_ended:
display(HTML("<p>Game over...</p>"))
return
display(HTML("<p>{} Guesses left.</p>".format(num_guesses_left)))
start_new_game_button.on_click(start_new_game)
submit_guess_button.on_click(submit_guess)
main()