Skip to content
Advertisement

Python Indentation with For, While and Try

I’ve written a small program to simulate a paper, scissor, rock game.

I’m using a for-loop to control the round-of-3 behaviour and am using while and try to control user input.

I don’t believe python is running the code within the game evaluation logic, but I’m not too sure why. I think it might have to do with scope / indentation, but I’m not sure? Have run out of ideas!

If anyone could point me in the direction of a previous answer, resource or help clarify the issue in my code I’d be very thankful.

# module packages
import random

def rockpaperscissor(): 

    # data structures and primary variables
    state = ["Rock", "Paper", "Scissor"]
    user_points = []
    computer_points = []
    round_counter = 1

    # game loop (best of three rounds)
    for i in range(3):
        print("Round " + str(round_counter) + ":")
        computer_choice = random.choice(state)
        # user input
        while True:
            try:
                print("You: ", end='')
                user_input = input().strip()
                if user_input not in {"Rock", "rock", "Paper", "paper", "Scissor", "scissor"}:
                    raise Exception
            except Exception:
                print("You have not entered a valid choice. Please re-enter.")
            else:
                break
        print("Computer: ", end='')
        print(computer_choice + "n")
        # game evaluation logic
        if user_input in {"Rock" or "rock"}:
            if computer_choice == "Paper":
                computer_points.append(1)
            elif computer_choice == "Scissor":
                user_points.append(1)
            elif computer_choice == user_input:
                computer_points.append(0)
                user_points.append(0)
        elif user_input in {"Paper" or "paper"}:
            if computer_choice == "Rock":
                print('test')
                user_points.append(1)
            elif computer_choice == "Scissor":
                computer_points.append(1)
                print('test')
            elif computer_choice == user_input:
                computer_points.append(0)
                user_points.append(0)
                print('test')
        elif user_input in {"Scissor" or "scissor"}:
            if computer_choice == "Paper":
                user_points.append(1)
            elif computer_choice == "Rock":
                computer_points.append(1)
            elif computer_choice == user_input:
                computer_points.append(0)
                user_points.append(0)
        round_counter = round_counter + 1
        print(user_points)
        print(computer_points)
    print("Your final score: ", end='')
    print(sum(user_points))
    print("The Computer's final score: ", end='')
    print(sum(computer_points))
    # outcome logic
    if user_points < computer_points:
        print("nSorry, you lost! Better luck next time!")
    elif user_points > computer_points:
        print("nCongratulations, you won!")
    else:
        print("nYou drew with the computer!")
    # repeat logic
    print("nDo you want to play again? Yes or No?")
    play_again = input().strip()
    print("n")
    if play_again in {"Yes", "yes"}:
        rockpaperscissor()
    elif play_again in {"No", "no"}:
        print("Not a problem. Thanks for playing. Catch you next time!")

rockpaperscissor()

# end of code

Advertisement

Answer

In addition to fixing your code, I’ve made some relatively minor changes to condense it, or tidy it up here and there:

# module packages
import random

state = ("rock", "paper", "scissors")

def get_input():
    while True:
        user_input = input("You: ").strip().lower()
        if user_input not in state:
            print("You have not entered a valid choice. Please re-enter.")
            continue
        break
    return user_input

def compare(move1, move2):
    """
    If draw, return 0
    If move1 beats move2, return 1
    If move2 beats move1, return -1
    """

    if move1 == move2:
        return 0
    if move2 == state[(state.index(move1) + 1) % 3]:
        return -1
    return 1

def rockpaperscissor():
    # data structures and primary variables
    user_points = 0
    computer_points = 0

    # game loop (best of three rounds)
    for round_counter in range(1, 4):
        print("nRound {}".format(round_counter))
        computer_choice = random.choice(state)
        # user input
        user_input = get_input()
        print("Computer: {}".format(computer_choice))

        # game evaluation logic
        result = compare(user_input, computer_choice)
        if result == 1:
            user_points += 1
        elif result == -1:
            computer_points += 1

        print("Round {} standings:".format(round_counter))
        print("User: {}".format(user_points))
        print("Computer: {}".format(computer_points))

    print("Your final score: {}".format(user_points))
    print("The Computer's final score: {}".format(computer_points))
    # outcome logic
    if user_points < computer_points:
        print("nSorry, you lost! Better luck next time!")
    elif user_points > computer_points:
        print("nCongratulations, you won!")
    else:
        print("nYou drew with the computer!")
    # repeat logic
    print("nDo you want to play again? Yes or No?")
    play_again = input().strip()
    print("n")
    if "y" in play_again.lower():
        rockpaperscissor()
    else:
        print("Not a problem. Thanks for playing. Catch you next time!")    

rockpaperscissor()
# end of code

I’m working on some explanations now.

The biggest problem in your code was trying to test user_input in {"Rock" or "rock"}. This is a pretty direct translation from spoken language to code – it would probably seem natural to ask “is the input a Rock or a rock?”. However, in Python, or is actually a special word used to operate on booleans (True or False values). For example, False or True is True, 1 == 4 or 6 < 7 is True, and so on. When Python tries to do "Rock" or "rock", it tries to treat both of the strings like booleans. Because they aren’t empty, they act like True, so they reduce to True, except because one of them is True, it reduces to one of them, roughly. This is a little confusing to those not too familiar with the idea of truthiness, and where it can be used, but the important thing is that the fix was to separate them with a comma ,, not or.

However, there’s an even better way to test this – using the .lower() method. Then you can directly do user_input.lower() == "rock". Note that this does mean "ROck" etc are also valid, but presumably that’s fine as in that case the user probably did mean rock. Now we can actually use this trick together with the previous explanation of the use of in, to combine the entire validation into user_input.lower() in state, as state is actually exactly the three things it can be (given that I’ve modified state to be lowercase).

I’ve also moved some of the stuff you’re doing into functions to improve clarity a bit.

get_input is now a function, using that validation logic. It also uses some simpler control flow than try/except – it uses the continue statement, which skips the rest of the body of the loop, and goes back to the start, so skips the break, and begins the whole cycle anew. If it is missed, the loops breaks and the function returns.

I’ve also written a function compare, which does the logic for comparing two moves. It uses a touch of voodoo – if move2 is to the right of move1 in state, move2 beats move1 so it returns -1, is what the longest condition does. It uses the modulo operator % so it “wraps” back around.

Using this compare function, the code can find out which score to increment without having to enumerate each case.

Aside from that, there are some smaller changes:

round_counter is drawn from the for loop, so it no longer has to be incremented.

I’m using .format a lot for string interpolation. It has relatively straightforward, concise syntax and is very powerful. I’d recommend using it in cases like this.

user_points and computer_points are just integers, as for the purposes of your code, being able to add 1 to them each time seems enough.

I’m verifying if the user wants to play again with 'y' in play_again.lower(), and actually have removed the elif condition, as it just needs to exit if the user gives no affirmation.

Also I’ve changed some statements of the form a = a + 1 to a += 1, which does the same thing but is slightly shorter.

As a comment mentioned, I’ve moved the import to the start of the file.

There is actually something stil very minorly sloppy about this code – rockpaperscissor is recursive. This isn’t inherently a problem, but as it could end up calling itself infinitely many times – if you find a very enthusiastic user – it will eventually actually crash. This is because when you go “deeper” into a function in Python, has to store the state of the whole “stack” of functions somewhere. This is a “stack frame” and Python only has a limited number of stack frames it can allocate before they overflow. Note that sometimes a new stack frame isn’t actually needed – your function is a case of that, as the information from any “parent” rockpaperscissor does not need to be preserved. This is called a “tail recursive” function, and many languages will actually optimise away the tail call, so this isn’t a problem. Unfortunately we in Python don’t have that luxury. So, if you like, you can try and change this code to use a while loop instead, so your hypothetical fan never has to stop playing.

This is what would end up happening:

Traceback (most recent call last):
  File "code.py", line 71, in <module>
    rockpaperscissor()
  File "code.py", line 29, in rockpaperscissor
    rockpaperscissor()
  File "code.py", line 29, in rockpaperscissor
    rockpaperscissor()
  File "code.py", line 29, in rockpaperscissor
    rockpaperscissor()
  [Previous line repeated 995 more times]
RecursionError: maximum recursion depth exceeded

You also mentioned being interested in practicing try/except statements. If you like, you could try adding code so that instead of crashing when a user from the command line hits Ctrl-C or Ctrl-D it exits gracefully, maybe saying goodbye.

They raise

Round 1
You: ^CTraceback (most recent call last):
  File "code.py", line 70, in <module>
    rockpaperscissor()
  File "code.py", line 38, in rockpaperscissor
    user_input = get_input()
  File "code.py", line 8, in get_input
    user_input = input("You: ").strip().lower()
KeyboardInterrupt

and

Round 1
You: Traceback (most recent call last):
  File "code.py", line 70, in <module>
    rockpaperscissor()
  File "code.py", line 38, in rockpaperscissor
    user_input = get_input()
  File "code.py", line 8, in get_input
    user_input = input("You: ").strip().lower()
EOFError

respectively.

Advertisement