CS21 Lab 6: More Advanced Functions

Due Sunday, March 13, by 11:59pm

NOTE: this is due at the end of Spring Break, but we strongly encourage you to try to finish before Spring Break starts (there won’t be any ninja sessions or office hours over break, so it’s best to get it done while you have access to help)

Programming Tips

As you write programs, use good programming practices:

  • Use a comment at the top of the file to describe the purpose of the program (see example).

  • All programs should have a main() function (see example).

  • Use variable names that describe the contents of the variables.

  • Write your programs incrementally and test them as you go. This is really crucial to success: don’t write lots of code and then test it all at once! Write a little code, make sure it works, then add some more and test it again.

  • Don’t assume that if your program passes the sample tests we provide that it is completely correct. Come up with your own test cases and verify that the program is producing the right output on them.

  • Avoid writing any lines of code that exceed 80 columns.

    • Always work in a terminal window that is 80 characters wide (resize it to be this wide)

    • In vim, at the bottom left in the window, there is an indication of both the line and the column of the cursor.

Are your files in the correct place?

Make sure all programs are saved to your cs21/labs/06 directory! Files outside that directory will not be graded.

$ update21
$ cd ~/cs21/labs/06
$ pwd
/home/username/cs21/labs/06
$ ls
Questions-06.txt
(should see your program files here)

Function Comments

All functions should have a top-level comment! Please see our function example page if you are confused about writing function comments.

Are your files in the correct place?

Make sure all programs are saved to your cs21/labs/06 directory! Files outside that directory will not be graded.

$ update21
$ cd ~/cs21/labs/06
$ pwd
/home/username/cs21/labs/06
$ ls
Questions-06.txt
(should see your program files here)

Goals

  • Work with functions and lists

  • Work with mutable and immutable function parameters

  • Work with functions that return values

  • Work with functions used only for their side effect

Curse Breaker

    \(\cdot\)     ansuz     \(\cdot\)     izaz     \(\cdot\)     sowilo     \(\cdot\)     hagalaz     \(\cdot\)     algiz     \(\cdot\)     ehwaz     \(\cdot\)     naudiz     \(\cdot\)    

For this week’s lab we’re going to incrementally develop an implementation of a game we’ll call Curse Breaker, in which the player is attempting to break a magical curse on an artifact. The primary task is to identify the specific ordered combination of magical runes used on a given artifact; however, the evil wizard has done their best to make this process difficult.

The runes used are hidden by magic, so the curse breaker must attempt to remove the curse without knowing what the runes are. Unfortunately, the curse will absorb power from spells cast at it, so after a certain number of attempts the artifact will explode! Fortunately, the curse breaker can interpret the colors thrown off by unsuccessful attempts; a clever curse breaker can use this feedback to discover the runes and remove the curse before it’s too late.

Our version of the rules.

In the game you’ll implement, the program generates a secret "curse", which is a sequence of four runes in Elder Futhark. To make our game simpler, we will restrict ourselves to seven possible runes:

  • ansuz (ansuz)

  • izaz (isaz)

  • sowilo (sowilo)

  • hagalaz (hagalaz)

  • algiz (algiz)

  • ehwaz (ehwaz)

  • naudiz (naudiz)

Since we have no easy way to type in the actual runes, we’ll have to settle for typing in these approximations of the pronunciations (i.e. the word in parens after the symbol).

The object of the game for the player is to guess the rune sequence, e.g. "sowilo isaz hagalaz ansuz". For our game, we’ll use 4 runes in the squence, and we’ll say each rune can only show up once (i.e. there are no duplicates in the sequence). Each turn, the player enters a guess, and the program then reports how many of the player’s guesses are correct (using "exact" matches, meaning the right rune in the right place).

The player has thirteen turns to guess the secret code. If they do, they win! Otherwise, the artifact explodes and the evil wizard wins!

For example, if the secret code is "sowilo isaz hagalaz ansuz", then:

  • a guess of "sowilo hagalaz ehwaz ansuz" has 2 matches.

  • a guess of "ehwaz sowilo ansuz isaz" has 0 matches.

  • a guess of "sowilo sowilo sowilo sowilo" has 1 match.

  • a guess of "hagalaz naudiz hagalaz naudiz" has 1 matches.

1. Printing lists of runes, generating lists of runes

Before writing functions to handle the main game mechanics, you should write some initial simple functions that generate and print lists of runes.

1.1. print_runes

The first function you write and test should be named print_runes(rune_list). It should take a single list rune_list of strings representing runes and nicely print the runes to the screen. Put vertical bars ('|') between the runes to make the printout look nice. For example,

  • if the input to print_runes was ["sowilo", "sowilo", "ansuz", "ehwaz"], then print_runes should print

| sowilo | sowilo | ansuz | ehwaz |
  • if the input to print_runes was ["hagalaz", "sowilo", "isaz", "algiz", "ehwaz", "naudiz"], then print_runes should print

| hagalaz | sowilo | isaz | algiz | ehwaz | naudiz |

Before moving onto other functions, test just this function by calling it within main(). For now, just make up the runes you want to print out.

1.2. get_rune

Once you’ve written and tested print_runes, write a function get_rune(runes, number). This function should take a list of the possible runes and a number, and should ask the user to choose a rune for the numbered position (see example below). Note that the rune number is mostly so when you print your prompts, you can let the user know what rune they’re getting (this will make more sense when you write the next function).

If the user enters something that’s not a legal rune, ask again, and keep on trying until the user enters a legal one. Once you’ve gotten one, return the rune that the user entered (as a string). "Legal" here just means it’s a rune that exists in the runes list given as input (HINT: remember the in operator works with lists). You’ll want to use print_runes to remind the user what the valid options are if they try to make an invalid choice (again, see examples below)

This function is an example of input validation; in this case, valid just means a string in our list of runes.

Test this function by itself; for example, if your main looks like this:

def main():
  rune_list = ["ansuz", "isaz", "sowilo", "hagalaz", "algiz", "ehwaz", "naudiz"]
  r = get_rune(rune_list, 1)
  print("you picked: " + r)

Some sample ouput might look like this:

$ python3 cursebreaker.py
Enter rune 1: hagalaz
you picked: hagalaz

$ python3 cursebreaker.py
Enter rune 1: zoom
zoom is not a valid rune.
Please choose from the following runes:
| ansuz | isaz | sowilo | hagalaz | algiz | ehwaz | naudiz |
Enter rune 1: blarg
blarg is not a valid rune.
Please choose from the following runes:
| ansuz | isaz | sowilo | hagalaz | algiz | ehwaz | naudiz |
Enter rune 1: isaz
you picked: isaz

1.3. get_guess

Once you’ve written and tested get_rune, you can write the function get_guess(runes, guess). This function should take one list of the possible runes and a second list to store the user’s guesses in; the function should then get 4 valid runes from the user, and place them into the guess list. Note that this function should call the previous one, it should not duplicate the functionality of get_rune. You’ll also want to use your print_runes function.

Note that this function will not return anything, but rather will modify the values in the guess list it takes as input; you can then use print_runes to print out the guess back in main.

See the example output below (user input in bold).

$ python3 cursebreaker.py
Please enter four legal runes.  Your choices are:
| ansuz | isaz | sowilo | hagalaz | algiz | ehwaz | naudiz |
Enter rune 1: sowilo
Enter rune 2: pineapple
pineapple is not a valid rune; please choose from the following runes:
| ansuz | isaz | sowilo | hagalaz | algiz | ehwaz | naudiz |
Enter rune 2: raido
raido is not a valid rune; please choose from the following runes:
| ansuz | isaz | sowilo | hagalaz | algiz | ehwaz | naudiz |
Enter rune 2: ehwaz
Enter rune 3: isaz
Enter rune 4: zzzz
zzzz is not a valid rune; please choose from the following runes:
| ansuz | isaz | sowilo | hagalaz | algiz | ehwaz | naudiz |
Enter rune 4: sowilo
You guessed:
| sowilo | ehwaz | isaz | sowilo |

$ python3 cursebreaker.py
Please enter four legal runes.  Your choices are:
| ansuz | isaz | sowilo | hagalaz | algiz | ehwaz | naudiz |
Enter rune 1: sowilo
Enter rune 2: ansuz
Enter rune 3: algiz
Enter rune 4: naudiz
You guessed:
| sowilo | ansuz | algiz | naudiz |

1.4. generate_code

Once you’ve written and tested get_guess(runes, guess), write and test a function generate_code(runes, code). This function should take a list of possible runes and a list of four empty strings; the second argument should be modified so that after the function it contains four random runes from the list, subject to the constraint that each rune in the list should be unique (i.e. there should be no repeats in the 4 chosen runes). Use the choice function from the random library. The choice function takes a list as input and returns a random element from the list. (HINT: again, think about how you can use the in operator to ensure you’re not about to add a rune that’s already in the code).

from random import choice
...
L=[2,3,5,7,11]
print(choice(L))  # prints out random number from L

Test your implementation of generate_code(runes) by calling it once or twice in main(). Use print_runes to print out the runes.

1.5. check_guess

Next, write a function that will check how many matches the player got with their guess. Note that we will be counting only "exact" matches, meaning the correct rune in the correct position. For example, if the code is isaz ansuz sowilo hagalaz and the user guesses ansuz algiz sowilo ehwaz, then only one match exists; the sowilo matches, but the ansuz is in the guess is in the wrong place so it doesn’t count.

Write a function check_guess(code, guess) that takes in two lists, each of which should contain four runes, and returns a count of how many matches there are. Remember, only count a match when the same rune is at the same place (HINT: you probably don’t want to use the in operator this time).

Be sure to test each function in your main() program before moving on.

2. Curse Breaker Game helper functions

By this point in the lab, you should have implemented and tested the following functions:

  • print_runes(rune_list)

  • get_rune(runes, number)

  • get_guess(runes, guess)

  • generate_code(runes, code)

  • check_guess(guess, code)

Before moving on to the final part of the implementation, it will be helpful to write and test some simple helper functions. Each of these functions will be quite simple, but they will help make the logic of your program easier to follow, and help avoid cluttering up main.

  • print_introduction() should take no parameters and print out the rules to the game Curse Breaker.

  • is_game_over(num_matches, turn) takes two integer input parameters. This function should return True if the game is over; that is, if the user guessed all four exact matches, or if the user ran out of their thirteen turns.

  • player_won(num_matches) takes one integer input parameter. This function should return True if the player won the game.

It might seem a little strange to test the is_game_over() or player_won() functions before writing the main part of the game. Just call these functions with dummy variables to see if they return what you expect them to return. It will be helpful to know these functions work before it comes time to complete your program.

3. Putting it all together

We’re almost all done now. The fully finished cursebreaker.py program might have the primary steps in main() as shown below. Think about how to use all the helper functions you’ve just implemented to accomplish each step.

  1. Initialize necessary variables (e.g. legal runes, etc.)

  2. Print out instructions to the user

  3. Pick a secret code for the user to guess.

  4. Until the user guesses the secret code or runs out of turns:

    • Show the current turn number

    • Ask the user for a guess

    • Compute the number of matches

    • Print out the number of matches

  5. When the game is over:

    • If the player won, print out a congratulatory message.

    • If the player ran out of turns, print out a sympathetic message.

4. Sample output

As with the previous lab, your program’s output does not need to be identical to the following output, but you should have all the required functionality, and your program should display the information in a readable way.

Here is sample output from a complete program: Sample Output

5. Answer the Questionnaire

Each lab will have a short questionnaire at the end. Please edit the Questions-06.txt file in your cs21/labs/06 directory and answer the questions in that file.

Once you’re done with that, run handin21 again.

6. Optional Extra Challenges

This is an optional extra challenge. There are lots of fun extensions you could add to cursebreaker.py.

  • Allow the user to choose how many turns the game takes.

  • Allow the user to choose how long the code is.

  • Allow the user to change how many runes you can guess from.

  • Implement a computer player who will make guesses instead of the user.

  • Anything else you can think of? Let us know if you come up with something interesting!

Answer the Questionnaire

Each lab will have a short questionnaire at the end. Please edit the Questions-06.txt file in your cs21/labs/06 directory and answer the questions in that file.

Once you’re done with that, you should run handin21 again.

Submitting lab assignments

Remember to run handin21 to turn in your lab files! You may run handin21 as many times as you want. Each time it will turn in any new work. We recommend running handin21 after you complete each program or after you complete significant work on any one program.

Logging out

When you’re done working in the lab, you should log out of the computer you’re using.

When Remotely logged in

When you are ssh’ed into the CS labs, first quit any applications you are running, like vim, then simply type exit at the prompt in your terminal window to disconnect.

When Physically logged in

When you are in a CS lab logged into a CS machine. First quit any applications you are running, like the browser and the terminal. Then click on the logout icon (logout icon or other logout icon) and choose "log out".

If you plan to leave the lab for just a few minutes, you do not need to log out. It is, however, a good idea to lock your machine while you are gone. You can lock your screen by clicking on the lock xlock icon. PLEASE do not leave a session locked for a long period of time. Power may go out, someone might reboot the machine, etc. You don’t want to lose any work!