--- jupytext: text_representation: extension: .md format_name: myst format_version: 0.13 jupytext_version: 1.10.3 kernelspec: display_name: Python 3 (ipykernel) language: python name: python3 --- +++ {"slideshow": {"slide_type": "slide"}} # Mastermind +++ {"slideshow": {"slide_type": "-"}, "tags": ["remove-cell"]} **CS1302 Introduction to Computer Programming** ___ ```{code-cell} ipython3 import random ``` +++ {"slideshow": {"slide_type": "subslide"}} In this notebook, you will write a game called [*Mastermind*](https://en.wikipedia.org/wiki/Mastermind_(board_game)). Play the video below to learn about the rule of the game. ```{code-cell} ipython3 --- code_folding: [0] slideshow: slide_type: '-' tags: [hide-input] --- %%html ``` 1. **Mastermind** first creates a hidden `code` of length `code_length` consisting code pegs with possibly duplicate colors chosen from a sequence of `colors`. 1. **Coderbreaker** provides a `guess` of the `code`. 1. **Mastermind** generates a `feedback` consisting 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 `code` is `'RBGG'` and `guess` is `'BGGG'`, then - the feedback should be `'bbw'` with - `black_key_pegs_count == 2` because of `__GG` in the guess, and - `white_key_pegs_count == 1` because of `_B__` in the guess. - `_G__` in the `guess` should not be awarded an additional white peg because `__GG` in the `code` has been counted. 1. **Codebreaker** wins if the code is correctly guessed within a certain number (`max_num_guesses`) of guesses. +++ {"slideshow": {"slide_type": "slide"}} ## 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. ```{code-cell} ipython3 :code_folding: [] :tags: [hide-input] %%html ``` 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. ```{code-cell} ipython3 for i in range(10): print(random.random()) # random floating point numbers in [0,1) ``` ```{code-cell} ipython3 for i in range(10): print(random.randint(3, 10), end=" ") # random integer in range [3,10] ``` ```{code-cell} ipython3 for i in range(10): print(random.choice("RBG"), end="") # random element in the sequence 'RBG' ``` We can generate a reproducible *pseudo-random* sequence by specifying the *seed*. ```{code-cell} ipython3 # repeatedly run the cell to see new sequences. random.seed(123456) for i in range(10): print(random.randint(3, 10), end=" ") ``` By default `random` uses the system time as the seed. We can call `seed` without any argument to revert to the default behavior. ```{code-cell} ipython3 # repeatedly run the cell to see new sequences. random.seed() for i in range(10): print(random.randint(3, 10), end=" ") ``` **Exercise** Define a function that generates a random `code`. The functions take in - a string `colors` whose characters represent distinct colors to choose from, and - a positive integer `code_length` representing 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'`. ```{code-cell} ipython3 --- deletable: false nbgrader: cell_type: code checksum: d59f8563fadcc14bd000b7ad3f4cc333 grade: false grade_id: get_code locked: false schema_version: 3 solution: true task: false tags: [remove-output] --- def get_code(colors, code_length): code = '' # YOUR CODE HERE raise NotImplementedError() return code ``` ```{code-cell} ipython3 --- code_folding: [] deletable: false editable: false nbgrader: cell_type: code checksum: 62902a60a692b540a10122e45e87c23a grade: true grade_id: test-get_code locked: true points: 1 schema_version: 3 solution: false task: false slideshow: slide_type: '-' tags: [remove-output, remove-cell] --- # hidden tests ``` +++ {"slideshow": {"slide_type": "subslide"}} ## Guess Validation +++ {"slideshow": {"slide_type": "fragment"}} **Exercise** Define a function `valid_code` that - takes `colors`, `code_length`, and `guess` as the first, second, and third arguments respectively, and - returns `True` if `guess` is a valid code, i.e., a string of length `code_length` with characters from those of `colors`, and - `False` otherwise. ```{code-cell} ipython3 --- deletable: false nbgrader: cell_type: code checksum: cf136637816a072f4c2cf89b0a55ebe0 grade: false grade_id: valid_code locked: false schema_version: 3 solution: true task: false slideshow: slide_type: skip tags: [remove-output] --- # YOUR CODE HERE raise NotImplementedError() ``` +++ {"slideshow": {"slide_type": "fragment"}} ````{hint} Solution template: ```Python 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 ``` ```` ```{code-cell} ipython3 --- code_folding: [0] deletable: false editable: false nbgrader: cell_type: code checksum: cbf569c8afbea2a08eb2305aafcc735c grade: true grade_id: test-valid_code locked: true points: 1 schema_version: 3 solution: false task: false slideshow: slide_type: skip tags: [remove-output, hide-input] --- # 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 ``` ```{code-cell} ipython3 --- deletable: false editable: false nbgrader: cell_type: code checksum: 6e1eac45d1d088c1f467f0d5ba8acd70 grade: true grade_id: hidden_test-valid_code locked: true points: 1 schema_version: 3 solution: false task: false tags: [remove-cell] --- # hidden tests ``` +++ {"slideshow": {"slide_type": "subslide"}} ## 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_counted` - `check_if_counted`, and - `reset_all_to_not_counted`. ```{code-cell} ipython3 --- deletable: false nbgrader: cell_type: code checksum: 29cd783448a79b5283dc57a836654b78 grade: false grade_id: markposition locked: false schema_version: 3 solution: true task: false tags: [remove-output] --- # YOUR CODE HERE raise NotImplementedError() ``` ```{code-cell} ipython3 --- code_folding: [0] deletable: false editable: false nbgrader: cell_type: code checksum: b1d9b6dbbc29fa2c568f18df0098c6f4 grade: true grade_id: test-markposition locked: true points: 1 schema_version: 3 solution: false task: false tags: [remove-output, hide-input] --- # tests reset_all_to_not_counted() mark_as_counted(3) assert check_if_counted(3) and not check_if_counted(0) ``` ```{code-cell} ipython3 --- deletable: false editable: false nbgrader: cell_type: code checksum: f70bbfcfc76bbfbcce386ca54cb96d78 grade: true grade_id: hidden_test-markposition locked: true points: 1 schema_version: 3 solution: false task: false tags: [remove-cell] --- # hidden tests ``` **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. ```{code-cell} ipython3 --- deletable: false nbgrader: cell_type: code checksum: 609c9d44fcf604b968abd407bf05b82f grade: false grade_id: reset_all_to_not_counted locked: false schema_version: 3 solution: true task: false tags: [remove-output] --- # YOUR CODE HERE raise NotImplementedError() ``` ```{code-cell} ipython3 --- code_folding: [0] deletable: false editable: false nbgrader: cell_type: code checksum: 27ef71bbc231faa498d5ae445378c50a grade: true grade_id: test-reset_all_to_not_counted locked: true points: 1 schema_version: 3 solution: false task: false tags: [remove-output, hide-input] --- # tests for i in range(11): assert not check_if_counted(i) if i % 2 else check_if_counted(i) ``` ```{code-cell} ipython3 --- deletable: false editable: false nbgrader: cell_type: code checksum: c4999cec10997e4055c0b558d13bbd46 grade: true grade_id: hidden_test-reset_all_to_not_counted locked: true points: 1 schema_version: 3 solution: false task: false tags: [remove-cell] --- # hidden tests ``` +++ {"slideshow": {"slide_type": "fragment"}} **Exercise** Define a function `get_feedback` that - takes `code` and `guess` as the first and second arguments respectively, and - returns 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). ```{code-cell} ipython3 --- deletable: false nbgrader: cell_type: code checksum: 578b868697e5a6346fa2935ba0008739 grade: false grade_id: get_feedback locked: false schema_version: 3 solution: true task: false slideshow: slide_type: skip tags: [remove-output] --- # YOUR CODE HERE raise NotImplementedError() ``` +++ {"slideshow": {"slide_type": "fragment"}} ````{hint} Solution template: ```Python 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 ``` ```` ```{code-cell} ipython3 --- code_folding: [0] deletable: false editable: false nbgrader: cell_type: code checksum: 7dcb41e05fc7ec16cf494e6612f280c1 grade: true grade_id: test-get_feedback locked: true points: 1 schema_version: 3 solution: false task: false slideshow: slide_type: skip tags: [remove-output, hide-input] --- # 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") ``` ```{code-cell} ipython3 --- deletable: false editable: false nbgrader: cell_type: code checksum: 453ed0a08437282653ce78edb0db8fa8 grade: true grade_id: hidden_test-get_feedback locked: true points: 1 schema_version: 3 solution: false task: false tags: [remove-cell] --- # hidden tests ``` +++ {"slideshow": {"slide_type": "slide"}} ## Play the Game +++ {"slideshow": {"slide_type": "fragment"}} After finishing the previous exercises, you can play the game as a code breaker against a random mastermind. ```{code-cell} ipython3 # 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 """
""".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( """

The code {} is invalid.
Leave the code box empty to randomly generated a code.

""".format( code ) ) ) is_game_ended = True else: num_guesses_left = max_num_guesses is_game_ended = num_guesses_left <= 0 display( HTML( "

Game started! {} Guesses left.

".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( """

Game has not started.
Please start a new game.

""" ) ) return if not valid_code(colors, code_length, guess): display(HTML("

Invalid guess.

")) return feedback = get_feedback(code, guess) num_guesses_left -= 1 with board: content = "" for k in guess: content += getPeg(k) content += """
Feeback:
""" for k in feedback: content += getPeg(k, 28) content += "
" display(HTML(content)) with message: message.clear_output() if feedback == "b" * code_length: is_game_ended = True display( HTML( "

You won with {} guesses left!

".format(num_guesses_left) ) ) return is_game_ended = num_guesses_left <= 0 if is_game_ended: display(HTML("

Game over...

")) return display(HTML("

{} Guesses left.

".format(num_guesses_left))) start_new_game_button.on_click(start_new_game) submit_guess_button.on_click(submit_guess) main() ```