CS21 Lab 4: Functions and while loops

Due Saturday, February 18 by 11.59PM

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.

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/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 for which for loops are not suitable

  • 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.

2. Programming Assignment: Dice Race!

For this week’s assignment, we’ll program a game called Dice Race! This is a two-player game, where the player is playing against the computer. Each player takes a turn at rolling a fair die, and advances a certain number of steps according to the rules described below. After a preset number of rounds, the player who has taken the most steps, wins.

Rules of Dice Race
  • The game starts with two players (one user versus one computer), and a preset number of rounds to play the game.

  • At each round, the players alternatively take turns at rolling a die.

  • The player whose turn it is, predicts the outcome of the die.

    • If the player’s prediction is less than the die’s outcome, then the player advances by one step.

    • If the player’s prediction is greater than the die’s outcome, then the player advances by the number of steps they predicted.

    • If the player’s prediction is the same as the die, then they do not advance!

  • The game ends when each player has completed the preset number of rounds.

  • The player with the most steps wins. If both the players have an equal number of steps, its a tie!

Since this is a more complex program than the ones we’ve implemented so far, 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 currently working on; when you get to the final stage, your main() function will play the entire game. For this program, we will keep the body of main() concise, and work on writing modular portions of our program, in other functions that are called by main.

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).

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

$ python3 dice_race.py
Welcome to Dice Race!
-----------------------------------------
The objective of the game is to get to the finish
line before your opponent, the computer.

First, provide your name, choose the number of rounds
of the game you would like to play.

Then, at each round, roll the die, and predict the
outcome of the die. If you predict a value less than
the outcome of the die, then you only get to advance
by one step. If you predict a value greater than the die
outcome you get to advance by the value
you predicted! Next, the computer gets to play by the
same rules.

To win the game, your task is to have more steps in
total, across all rounds, than the computer. Good Luck!
-----------------------------------------

2.2. Checking if a die roll prediction is valid

Write a function called get_prediction that takes a potential prediction of the die roll from the user, and checks whether or not it is a valid choice. "Valid" in this case means that the user chose an integer in the range from 1 to a 6(inclusive) — i.e., all possible values that a die can take.

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.

The prototype for the function should look like this:

def get_prediction():
    """
    Purpose: Get the user's prediction of the die roll
    Parameters: none
    Returns: (int) - Positive integer between 1 and 6
    """

When testing a function like this, be sure to check the edge cases, which are the values right around the limits of the range of accepted values, 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 minimum and maximum values.

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

def main():
  player_choice = get_prediction()
  print("%s chose %d" %(player_name, player_choice))
Remember at this point, we are simply testing for valid input. We are not rolling a die yet!

The output of a few runs of our input validation might look like this:

$ python3 dice_race.py
Predict the die outcome: 1
Player chose 1.

$ python3 dice_race.py
Predict the die outcome: 3
Player chose 3.

$ python3 dice_race.py
Predict the die outcome: 6
Player chose 6.

$ python3 dice_race.py
Predict the die outcome: -1
Sorry, -1 is not a valid choice.
Please enter a positive integer value between 1 and 6.
Predict the die outcome: 4
Player chose 4.

$ python3 dice_race.py
Predict the die outcome: 10
Sorry, 10 is not a valid choice.
Please enter a positive integer value between 1 and 6.
Predict the die outcome: 2
Player chose 2.

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 get_prediction 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 dice_race.py
Predict the die outcome: hello
Sorry, hello is not a valid choice.
Please enter a positive integer value between 1 and 6.
Predict the die outcome: 1
Player chose 1.

$ python3 dice_race.py
Predict the die outcome: six
Sorry, six is not a valid choice.
Please enter a positive integer value between 1 and 6.
Predict the die outcome: 6
Player chose 6.

$ python3 dice_race.py
Predict the die outcome: 4.7
Sorry, 4.7 is not a valid choice.
Please enter a positive integer value between 1 and 6.
Predict the die outcome: 4
Player chose 4.

2.3. Rolling a die

Let’s now write a function roll_die to roll the die for each player. To do so, we will import the randint function from the random module. randint(a, b) will return a random integer between a and b (inclusive). For example:

from random import randint

# Randomly select an integer between 3 and 10 (inclusive)
random_value = randint(3, 10)
print(random_value)

# This will print something different every time from the set {3, 4, 5, 6, 7, 8, 9, 10}

The prototype for the function should look like this:

def roll_die():
  """
  Your Function Comments Here
  Purpose:
  Parameters:
  Returns:
  """

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

def main():
  die_outcome = roll_die()
  print("Here's the outcome from one die roll: " + str(die_outcome))

The output might look like this:

$ python3 dice_race.py
Here's the outcome from one die roll: 1

$ python3 dice_race.py
Here's the outcome from one die roll: 4

$ python3 dice_race.py
Here's the outcome from one die roll: 6

$ python3 dice_race.py
Here's the outcome from one die roll: 3

2.4. Printing the player prediction and die outcome

Let’s now write a function called print_outcome(), that prints both the player’s prediction of the die roll and the actual outcome. The prototype for the function should look like this:

def print_outcome(player_name, player_choice, die_outcome):
  """
  Your Function Comments Here
  Purpose:
  Parameters:
  Returns:
  """

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

def main():
  player_name = "Lauri"
  player_choice = 4
  die_outcome = 2
  print_outcome(player_name, player_choice, die_outcome)

The output might look like this for the example shown above:

$ python3 dice_race.py
Lauri choose 4 and the die result was 2.

2.5. Advancing each player

Next, write a function called advance_player() that adds the correct number of steps to advance the player, and returns the updated total number of steps for the player. In the body of the function, make sure to check if the player’s choice is less than or greater than the die result and if the player’s choice is equal to die result. For each branching condition, add the appropriate number of steps according to the game’s rules.

The prototype for the function should look like this:

def advance_player(die_outcome, player_choice, player_steps):
  """
  Purpose: Perform a roll of a fair die
  Parameters: player_choice, die_result, player_steps
  Return: player_steps: the updated accumulator value of the
          number of steps the player has taken
  """

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

def main():
  player_name = "Vasanta"
  die_outcome = 4
  player_choice = 3
  player_steps = 2
  player_steps = advance_player(die_outcome, player_choice, player_steps)
  print("Total steps for %s are %d" %(player_name, player_steps))

For the example values shown above, since the die outcome is greater than the player’s prediction, the player only advances by 1 step. Therefore, the total step count, after the call to advance_player would now be 3. The output might look like this:

$ python3 dice_race.py
Total steps for Vasanta are: 3

2.6. Programming the computer’s prediction

Congratulations for making it this far! We’ve now got everything we need for one player to play Dice Race!

Let’s now write a function computer_prediction() that programs our opponent - the computer’s prediction. We will assume that the computer will simply pick a random integer between 1 and 6. We’ve already seen how we can select a random integer using randint. We will use the same logic here, to return the computer’s choice.

Here’s how you might call and test your function in main:

def main():
  computer_choice = computer_prediction()
  print("The computer's prediction is: " + str(computer_choice))

The output might look like this:

$ python3 dice_race.py
The computer's prediction is: 3

$ python3 dice_race.py
The computer's prediction is: 5

$ python3 dice_race.py
The computer's prediction is: 1

2.7. Printing scores for each round

For each round of the game we also would like to print scores for both the players. Let’s write a function called print_scores() that prints the scores for both the players. The prototype for the function should look like this:

def print_scores(player_name, player_steps, computer_steps):
  """
  Your Function Comments Here
  Purpose:
  Parameters:
  Returns:
  """

Note that since we are playing against the computer, we aren’t explicitly passing in the names of two players.

Here’s how you might call and test your function in main:

def main():
  player_name = "Crystal"
  current_round = 2
  player_steps = 3
  computer_steps = 5
  print_scores(player_name, player_steps, computer_steps)

The output might look like this:

$ python3 dice_race.py
Crytal's Total Steps: 3
Computer's Total Steps: 5

2.8. End-of-game messages

Write a function game_over that takes in the names of all the players, and the total steps that each player has accumulated.

The output of this function might look like this.

==================================
We've reached the end of the game!
The champion is: Lauri!

2.9. Putting it all together: Playing the game

Now that you have all the functions you need, let’s combine them to play multiple rounds of Dice Race against a computer opponent. Be sure that all of the functions you write for this lab are well commented — if you haven’t done so already, now would be a good time to write those comments, as they may help you to put all the pieces together.

2.10. The main function

The main function of your program should now use the functions that we have built so far to construct the complete game.

Structuring your code

The following tips are to help use the functions we have written to write clean, modular code.

  • In main() our goal is going to be to reuse the functions we have written so far to handle the logic of both the user and the computer to play!

  • For example, we might want to use roll_die and advance_player functions for both the user and the computer.

  • Similarly, we will want to use the same print functions to keep track of the outcomes for the user and the computer.

  • Read through the steps below to construct your main function - and remember that main is primarly responsible for keeping track of the rounds of play, and within each round we should be calling the functions that we have already written.

Overall flow of main:

  • Ask the player for their name and the number of rounds to play. You may assume the player enters a valid positive integer for the number of rounds.

  • Continue playing the game for the number of requested rounds, keeping track of the number of steps for the human player and the computer. Use string formatting when printing results.

  • In each round, make use of the get_prediction or computer_prediction as appropriate for the human player and the computer.

  • For each round, use the roll_die, computer_prediction, and advance_player functions to calculate the player’s game progress.

  • Use your functions to generate the die roll and the computer’s prediction.

  • Use the print functions: print_outcome, and print_scores for each round, and finally use game_over once all the rounds are complete to declare the winner.

2.10.1. Example Output

Here is an example of run of the complete game:

$ python3 dice_race.py
Welcome to Dice Race!
-----------------------------------------
The objective of the game is to get to the finish
line before your opponent, the computer.

First, provide your name, choose the number of rounds
of the game you would like to play.

Then, at each round, roll the die, and predict the
outcome of the die. If you predict a value less than
the outcome of the die, then you only get to advance
by one step. If you predict a value greater than the die
outcome you get to advance by the value
you predicted! Next, the computer gets to play by the
same rules.

To win the game, your task is to have more steps in
total, across all rounds, than the computer. Good Luck!
-----------------------------------------

Please enter your name: Rain
How many rounds would you like to play? 4

------Starting the game--------

------Round Number: 1-----------
Predict the die outcome: 5
Rain choose 5 and the die result was 1
Computer choose 6 and the die result was 3

Rain's Total Steps: 5
Computer's Total Steps: 6

------Round Number: 2-----------
Predict the die outcome: 6
Rain choose 6 and the die result was 6
Computer choose 1 and the die result was 2

Rain's Total Steps: 5
Computer's Total Steps: 7

------Round Number: 3-----------
Predict the die outcome: 2
Rain choose 2 and the die result was 2
Computer choose 1 and the die result was 4

Rain's Total Steps: 5
Computer's Total Steps: 8

------Round Number: 4-----------
Predict the die outcome: 6
Rain choose 6 and the die result was 3
Computer choose 1 and the die result was 5

Rain's Total Steps: 11
Computer's Total Steps: 9
==================================
We've reached the end of the game!
The champion is: Rain!

3. 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!