CS21 Lab 4: Functions and while loops

Due Saturday, October 1, by midnight

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 vscode, at the bottom right 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/04 directory! Files outside that directory will not be graded.

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

Goals

The goals for this lab assignment are:

  • Develop your understanding of the stack and how function calls and returns work

  • Write programs with multiple functions

  • Use indefinite while loops to solve problems that for loops aren’t suited for

  • Use the random library to make pseudo-random choices

1. Written Assignment: Stack Diagram

The first part of this lab is a written assignment to trace through some Python code, show the program output and draw the stack. Download the following .pdf file, print it out: lab4stack.pdf

You should write your solution on the print out, and submit it at the start of the class on Friday.

For this week’s assignment, we’ll program a game called Cookie Jar. The goal of the game is to take cookies out of the jar, but not be the one to take the last cookie since, as we all know, whoever takes the last cookies has to refill it, and everybody wants that to be someone else’s problem. The rules we’ll play by are as follows:

  • The game starts with 12 cookies in the jar

  • There are two players, who alternate turns taking cookies from the jar

  • The player who’s turn it is decides how many cookies to take from the jar

  • The player must take at least one cookie, but cannot take more than three during any given turn (and also cannot take more cookies than remain in the jar).

  • The game ends when the jar is empty

  • The player who took the last cookie looses; the other player wins

Since this is a more complex program than the ones we’ve implemented previously, it will be even more important to practice good incremental development. Follow the development plan given below and implement one function at a time; each time, be sure to test your function thoroughly and make sure it works before moving on to the next one.

Your main() function should be used to test whatever function you are working on; when you get to the final stage, your main() function will play the entire game, but we will be keeking main() small in this program.

2.1. Welcome Message

Write a function called welcome that prints out a message welcoming the players to the game and explaining the rules. This function takes no arguments, and returns no value; it should just contain print statements. The prototype for the function should look like this:

def welcome():
Remember that all functions, including this one, should have a block comment at the top explaining what the function does, as well as any parameters or return values (though in this case you don’t have either of those).

To test this function, call it in main(). Here’s an example of what the output should look like:

$ python3 cookie_jar.py
========================================
Welcome to the Cookie Jar game!

Whoever takes the last cookie has to go buy more, so the objective
of the game is to take as many cookies as you can without being the
one who takes the last cookie.  You also can't take more than three
cookies at a time, since that's just rude.
========================================

2.2. Checking if a choice is valid

Write a function called is_valid that takes a potential choice from the user (presumably obtained via a call to input), and checks whether or not it is a valid choice. "Valid" in this case means it is an integer in the range from min_val to max_val (inclusive).

If the choice is invalid, the function should print a helpful message saying what went wrong (see examples below). If the choice is valid, the function doesn’t need to print anything. Either way, the function should return True or False to indicate whether the input is valid or not.

The prototype for the function should look like this:

def is_valid(choice, min_val, max_val):
    """
    Checks a user choice to see if it's "valid" (i.e. between the bounds)
    parameters:
        choice (str) - the value to check
        min_val, max_val (int) - the lower and upper bounds of the "valid"
            range (inclusive)
    returns: (boolean) - True if choice is valid, False otherwise
    """

When testing a function like this, be sure to check the edge cases, which are the values right around where the behavior should change. In this case, you’ll want to test what happens for values exaclty equal to, one less than, and one greater than, the min_val and max_val parameters.

Test this function by calling it in main(); for example, your main might look like this:

def main():
  choice = input("choice? ")
  print(is_valid(choice, 1, 3))

And the output of a few runs might look like this:

$ python3 cookie_jar.py
choice? 1
True
$ python3 cookie_jar.py
choice? 2
True
$ python3 cookie_jar.py
choice? 3
True
$ python3 cookie_jar.py
choice? 4
Sorry, you can't take 4 cookies; value must be between 1 and 3
False
$ python3 cookie_jar.py
choice? 5
Sorry, you can't take 5 cookies; value must be between 1 and 3
False
$ python3 cookie_jar.py
choice? 0
Sorry, you can't take 0 cookies; value must be between 1 and 3
False

2.2.1. (OPTIONAL) Checking if a string is an integer

While checking the range of a value is a good first step, the program will still crash if the user types in something that’s not an integer. As an optional extension, you can modify your is_valid function to safely detect and handle non-integer inputs. Note that the way we call and use the function will be the same, so it’s recommended to finish the rest of the lab first and then come back to this part if you have time (since it’s optional and won’t count against you if you don’t have time).

Detecting numbers

To check if a string can safely be converted to an int, you can ask whether all the characters in the string are digits:

choice = input("give me a value")
if (choice.isdigit()):
  print("choice contains only digits, and can be cast to an int")
else:
  print("choice contains non-digit characters")

Note that the syntax here is a bit different than we’ve seen before; you need to use the name of the variable you want to check, then a period, then the name of the function you want to use. In this example, choice is the name of a variable containing a string, and isdigit is the function we want to run. We’ll learn more about what this syntax means and how it works in the coming weeks; for now, just know that this is how you check if a string is made of digits.

Note also that .isdigit() will evaluate to True only when all the characters in the string are digits; this means that floating point numbers and negative numbers will produce False, since characters like . or - are not digits.

If you choose to do the optional extra of detecting non-numeric inputs, the output of some testing runs might look like this:

$ python3 cookie_jar.py
choice? hello
Sorry, you can't take hello cookies; value must be an integer.
False
$ python3 cookie_jar.py
choice? three
Sorry, you can't take three cookies; value must be an integer.
False
$ python3 cookie_jar.py
choice? 4.7
Sorry, you can't take 4.7 cookies; value must be an integer.
False
$ python3 cookie_jar.py
choice? -3
Sorry, you can't take -3 cookies; value must be an integer.
False

2.3. Getting a valid choice

Next, write a function called get_choice that will ask the player for a number of cookies to take, and then use is_valid to check if it’s valid or not. Be sure you take into account the number of cookies remaining in the jar; if cookies is less than max_val you’ll need to be careful when you call is_valid. For a valid choice, the function should return the choice (as an integer). For an invalid choice, the program should continue to re-prompt the user for as long as it takes to get a valid choice (this will require a while loop).

The prototype for the function should look like this:

def get_choice(min_val, max_val, player, cookies):

Test it by calling the function in main; e.g.

def main():
  choice = get_choice(1, 3, "Ada", 5)
  print("Choice is: " + str(choice))

The output might look like this:

$ python3 cookie_jar.py
How many cookies does Ada want to take? 0
Sorry, you can't take 0 cookies; value must be between 1 and 3
Please enter a value between 1 and 3: 4
Sorry, you can't take 4 cookies; value must be between 1 and 3
Please enter a value between 1 and 3: 5
Sorry, you can't take 5 cookies; value must be between 1 and 3
Please enter a value between 1 and 3: 2
Choice is: 2

If there were only 2 cookies left in the jar, notice how you can no longer accept 3 cookies as a valid response, e.g.:

def main():
  choice = get_choice(1, 3, "Ada", 2) # 2 cookies left
  print("Choice is: " + str(choice))

The output might look like this:

$ python3 cookie_jar.py
How many cookies does Ada want to take? 0
Sorry, you can't take 0 cookies; value must be between 1 and 2
Please enter a value between 1 and 2: 3
Sorry, you can't take 4 cookies; value must be between 1 and 2
Please enter a value between 1 and 2: 2
Choice is: 2

2.4. Printing current game status

Write a function print_status that takes three arguments, a player name, the current turn number, and the number of cookies in the jar; it should print out a status message describing the game state. For example:

def main():
  print_status("Ada", 2, 9)

Might produce output like:

----------
Turn 2 - Jar contains 9 cookies
It's Ada's Turn

2.5. Tracking who’s turn it is

Write a function get_player that takes in the turn number and a list of player names. It should return the name of the player who’s turn it is. Assume that the player whos name is at at list index 0 goes first; this means that you can figure out who’s turn it is by using the modulus (%) operator, since turn % 2 should give you the list index of the player whos turn it is.

Note that this function will likely be very short (possibly only one or two lines), but it’s worth writing anyway since it will make the program easier to read and understand.

def main():
  players = ["Ada", "Beth"]
  print(get_player(4, players))
  print(get_player(7, players))

Might produce output like:

Ada
Beth

2.6. End-of-game messages

Write a function game_over that takes in the names of the winning and losing players, and prints out a message saying the game is over and who won and lost. Output might look like:

### The cookie jar is empty! ###
Ada takes the last cookie and must go buy another box!
Beth wins!

remember that you can use get_player to get the names to pass to this function.

2.7. Putting it all together: Playing the game

Finally, write a function called play_game that contains the main game loop and plays the game. It should take in a list containing two player names

This function should make use of the functions you’ve already written; it should not contain code that duplicates the code in other functions.

The prototype should look like:

def play_game(players, min_take, max_take, cookies):

And your final version of main should look something like:

def main():
  welcome()

  players = ["a", "b"]
  players[0] = input("What is the first player's name? ")
  players[1] = input("What is the second player's name? ")

  play_game(players, 1, 3, 12)

2.7.1. Example Output

Here is an example of running the completed game:

$ python3 cookie_jar.py
========================================
Welcome to the Cookie Jar game!

Whoever takes the last cookie has to go buy more, so the objective
of the game is to take as many cookies as you can without being the
one who takes the last cookie.  You also can't take more than three
cookies at a time, since that's just rude.
========================================

What is the first player's name? Ada
What is the second player's name? Beth
----------
Turn 0 - Jar contains 12 cookies
It's Ada's Turn
How many cookies does Ada want to take? 3
----------
Turn 1 - Jar contains 9 cookies
It's Beth's Turn
How many cookies does Beth want to take? 2
----------
Turn 2 - Jar contains 7 cookies
It's Ada's Turn
How many cookies does Ada want to take? 24
Sorry, you can't take 24 cookies; value must be between 1 and 3
Please enter a value between 1 and 3: 0
Sorry, you can't take 0 cookies; value must be between 1 and 3
Please enter a value between 1 and 3: 2
----------
Turn 3 - Jar contains 5 cookies
It's Beth's Turn
Please enter a value between 1 and 3: 3
----------
Turn 4 - Jar contains 2 cookies
It's Ada's Turn
How many cookies does Ada want to take? 3
Sorry, you can't take 3 cookies; value must be between 1 and 2
Please enter a value between 1 and 2: 1
----------
Turn 5 - Jar contains 1 cookies
It's Beth's Turn
How many cookies does Beth want to take? 0
Sorry, you can't take 0 cookies; value must be between 1 and 1
How many cookies does Beth want to take? 2
Sorry, you can't take 0 cookies; value must be between 1 and 1
Please enter a value between 1 and 1: 1

### The cookie jar is empty! ###
Beth takes the last cookie and must go buy another box!
Ada wins!

(Optional) Extension: more game options

For an extra challenge, copy your program to the file cookie_jar_extra.py and try to add some (or all!) of the following modifications to this new file. Note that they are listed in rough order of increasing difficulty:

  • At the start of the game, let the user input the initial number of cookies in the jar (instead of just always using 12)

  • At the start of the game, let the user input the maximum number of cookies that can be taken at one time (instead of always using 3)

  • Keep track of the total number of cookies each player has taken, and print out the totals when the game is over (e.g. "Ada got a total of 7 cookies")

  • Make a version of the game with three players instead of just two

  • Make a version of the game where the user can specify how many players will participate at the start of the game

Answer the Questionnaire

Each lab will have a short questionnaire at the end. Please edit the Questions-04.txt file in your cs21/labs/04 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.

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!