Python Noob - Looking for anti-spaghetti suggestions and avoiding redundant code - python-3.x

First post here. I wanted to get this out to you folks because I'm looking for some insightful feedback and my approaches coming back into this world. My main experience is with Javascript on front-end work, and I hadn't written a single letter of python until yesterday afternoon, so excuse the mess.
I'm trying to see what you might see as some right-off-the-bat suggestions for abbreviating, or cleaning up this code. Maybe some glaring "please never do this again" items, or whatever insight you might have.
It's a console number guessing game, currently 1 - 10. It does run, with some bugs. The one main one that keeps tripping me up (and I'm sure it's simple) is if you guess the correct answer on the last try (3rd in this case) it does not run the 'Congrats' code. It runs the 'You failed' section.
Just looking for some suggestions or input of any kind to help assess MY thinking and approach to problem solving in this language. Also, if there's a better place on the web for this kind of nooby-ness, let me know. Thanks!
from random import randint
def guessing_game():
# Set the count and limits for the loop and game length.
guess_count = 0
guess_limit = 2
# Ask the user to take a guess.
guess = int(input("I'm thinking of a number between 1 and 10... "))
# Randomize the answer. Change the range according to preference.
the_answer = randint(1, 10)
# Start the loop
while guess_count < guess_limit:
if guess == the_answer: # The user wins and is prompted to play again. Init function would probably be useful.
print(f"Congrats, you won! The answer is {the_answer}!")
restart = input("Play again? ")
if restart.upper() == "Y":
guessing_game()
else: # The user declines to play and we break out of the loop.
print("Okay, thanks for playing! ")
break
break
elif guess != the_answer: # The user guessed the wrong answer. Ask again and add to the count.
print("Oops, try again.")
guess = int(input("I'm thinking of a number between 1 and 10... "))
guess_count += 1
if guess_count == guess_limit: # Shucks, the user failed. Inform them of their failure and offer redemption.
print('Sorry, you failed.')
restart = input("Play again? ")
if restart.upper() == "Y":
guess_count = 0
guessing_game()
else: # The user opted out of continuing this exciting adventure.
print("Okay, thanks for playing! ")
break
guessing_game()
1/23/2020 - 12:45pm EST Notice the updated code below per #An0n1m1ty. With this updated code I can make up to 4 incorrect guesses before it terminates. On the second wrong guess, there is no 'Oops' message printed, and on the fourth wrong guess, the program just ends. No 'You lost' message or prompt to play again.
Winning, however, seems to be functioning correctly. I will go over it and try to identify the logic behind the current behavior. You can see the changes I made with comments starting with 'CHANGE'.
Also, if there's a better place to display these code updates, please let me know. I didn't put it in an answer, because it's not an answer. Sorry, I'm a noob.
from random import randint
def guessing_game():
# Set the count and limits for the loop and game length.
guess_count = 0
guess_limit = 2
# CHANGE Moved guess input from here to beginning of loop
# Randomize the answer. Change the range according to preference.
the_answer = randint(1, 10)
# Start the loop
while guess_count < guess_limit:
# CHANGE Ask the user to take a guess.
guess = int(input("I'm thinking of a number between 1 and 10... "))
if guess == the_answer: # The user wins and is prompted to play again. Init function would probably be useful.
print(f"Congrats, you won! The answer is {the_answer}!")
restart = input("Play again? ")
if restart.upper() == "Y":
guessing_game()
else: # The user declines to play and we break out of the loop.
print("Okay, thanks for playing! ")
break
break
elif guess != the_answer and guess_count != guess_limit: # CHANGE The user guessed the wrong answer.
print("Oops, try again.")
guess = int(input("I'm thinking of a number between 1 and 10... "))
guess_count += 1
elif guess_count == guess_limit: # CHANGE Shucks, the user failed. Inform them of their failure and offer redemption.
print('Sorry, you failed.')
restart = input("Play again? ")
if restart.upper() == "Y":
guess_count = 0
guessing_game()
else: # The user opted out of continuing this exciting adventure.
print("Okay, thanks for playing! ")
break
guessing_game()
1/23/2020 - 7:45pm EST So with these changes we are a lot closer. The main issue is that if you get three wrong answers, the program just terminates. No failure message or prompt to play again. I believe this is because the loop condition is met after three tries, so the loop breaks and the
elif guess_count == guess_limit:
section never has a chance to run. I solved this issue by changing
while guess_count < guess_limit:
to
while guess_count <= guess_limit:
which allows the aforementioned section to run and prompt to play again. The only thing with this solution is that the loop runs one more time beyond the guess_limit because of the <= at the beginning of the loop. So you have to reduce the guess_limit by 1 of what you actually want.
The problem I'm having now is that if I go through a round and lose (only) and choose to play again, after losing or winning that next round and choosing NOT to continue, it will say "Okay, thanks for playing" and the game immediately starts over instead of terminating. So that's where I'm at now.
Thanks so much for all your help! Sounds weird, I'm sure, but it has me looking at it fresh again.

The following code is where the issue is:
elif guess != the_answer: # The user guessed the wrong answer. Ask again and add to the count.
print("Oops, try again.")
guess = int(input("I'm thinking of a number between 1 and 10... "))
guess_count += 1
When the user finally gets the answer right, the next bit of code will execute:
if guess_count == guess_limit: # Shucks, the user failed. Inform them of their failure and offer redemption.
print('Sorry, you failed.')
restart = input("Play again? ")
if restart.upper() == "Y":
guess_count = 0
guessing_game()
else: # The user opted out of continuing this exciting adventure.
print("Okay, thanks for playing! ")
break
Now, if the user is on the 3rd guess, guess_count == guess_limit will be true and the code will execute saying "Sorry, you failed"
To fix the issue, I did the following:
Move
guess = int(input("I'm thinking of a number between 1 and 10... "))
to the first line of the loop. i.e.:
while guess_count < guess_limit:
guess = int(input("I'm thinking of a number between 1 and 10... "))
Then, change:
elif guess != the_answer:
to
elif guess != the_answer and guess_count != guess_limit:
Then, change:
elif guess != the_answer and guess_count != guess_limit: # CHANGE The user guessed the wrong answer.
print("Oops, try again.")
guess = int(input("I'm thinking of a number between 1 and 10... "))
guess_count += 1
to
elif guess != the_answer and guess_count != guess_limit: # CHANGE The user guessed the wrong answer.
print("Oops, try again.")
guess_count += 1
Then, change:
guess_limit = 2
to
guess_limit = 3
Finally, change:
if guess_count == guess_limit:
to
elif guess_count == guess_limit:

Manual while loops are almost always the wrong answer in Python. It's much simpler to do a for loop over a range that runs up to the number of times specified. In addition, Python for loops can take an else block that executes when the loop runs to completion, but doesn't execute if you break (or return, or an exception bubble out), making them ideal for "needle in a haystack" scenarios where the needle wasn't found (while loops can have else blocks as well, but for loops are the better option here). It also lets you move common code (code that executes whether they guessed correctly or not, in this case, the "new game" check) outside both for and else, so you either output one set of data within the loop and break to the common code, or the loop runs to completion, and the else block outputs a different message before the common code.
So you could could be dramatically simplified by using a for/else to:
from random import randint
def guessing_game():
# Set the count and limits for the loop and game length.
guess_limit = 2 # Allows up to two guesses
# Randomize the answer. Change the range according to preference.
the_answer = randint(1, 10)
for guess_count in range(guess_limit):
guess = int(input("I'm thinking of a number between 1 and 10... "))
if guess == the_answer:
print(f"Congrats, you won! The answer is {the_answer}!")
break # Go to common code for retry
elif guess_count != guess_limit: # Don't need to retest guess != the_answer; this is an else that never executes unless they weren't equal
print("Oops, try again.")
# Don't need to prompt for input again, we'll prompt at top of loop
else:
# Only prints if loop ran to completion without breaking
print('Sorry, you failed.')
# Common code run for both success and failure:
restart = input("Play again? ")
if restart.upper() == "Y":
guessing_game()
else:
print("Okay, thanks for playing! ")
if __name__ == '__main__': # Get in the habit of using the import guard now; it's not necessary yet, but when it is, you want to be in the habit
guessing_game()
Note that the unbounded recursion here is potentially problematic. If the user plays close to a thousand times, you'll hit the Python recursion limit. If that's a concern, I'd refactor to separate "one game" from "all games", and have "all games" be implemented as a loop, so you don't need to recurse to play another game:
from random import randint
def play_one_game():
# Set the count and limits for the loop and game length.
guess_limit = 2 # Allows up to two guesses
# Randomize the answer. Change the range according to preference.
the_answer = randint(1, 10)
for guess_count in range(guess_limit):
guess = int(input("I'm thinking of a number between 1 and 10... "))
if guess == the_answer:
print(f"Congrats, you won! The answer is {the_answer}!")
break # Go to common code for retry
elif guess_count != guess_limit: # Don't need to retest guess != the_answer; this is an else that never executes unless they weren't equal
print("Oops, try again.")
# Don't need to prompt for input again, we'll prompt at top of loop
else:
# Only prints if loop ran to completion without breaking
print('Sorry, you failed.')
def guessing_game():
keep_playing = True
while keep_playing:
play_one_game()
# Common code run for both success and failure:
keep_playing = input("Play again? ").upper() == "Y"
print("Okay, thanks for playing! ")
if __name__ == '__main__': # Get in the habit of using the import guard now; it's not necessary yet, but when it is, you want to be in the habit
guessing_game()
This approach also helps minimize complexity; guessing_game used to do two barely related things:
Play a single instance of the game
Determine whether or not to play another game
Separating them out keeps the two behaviors separated, so understanding either feature is easier in isolation.
One last suggestion: There's no great way to handle the "don't give the Oops message on the last loop" case I can think of off-hand, but there are "not awful" ways to remove the need for a test that will fail on most loops. The simplest approach is to echo the extra message as part of the input, using an extra message that's initially empty, and unconditionally set to the "oops" message afterwards, so it's used for all subsequent loops. To do so, you'd just replace:
for guess_count in range(guess_limit):
guess = int(input("I'm thinking of a number between 1 and 10... "))
with:
extra = '' # No oops message the first time
for _ in range(guess_limit): # Don't even name guess_count, we never use it; by convention, _ means unused variable
guess = int(input(extra + "I'm thinking of a number between 1 and 10... "))
extra = "Oops, try again.\n" # From now on, oops incorporated into prompt
allowing you to completely remove:
elif guess_count != guess_limit:
print("Oops, try again.")
replacing a per loop conditional branch with unconditional execution paths (which also makes code easier to test, since there are fewer unused or lightly used paths).

Related

Trying to exit from a program is not working

I'm currently teaching myself python from books, YouTube, etc. Recently, I've started from Python Tutorial - Python for Beginners [Full Course]. I've been trying to make some bigger programs by myself (learning from python documentation) than the ones in this video. Hence, this is my question:
I'm trying to find a solution for how to exit from a function the_func after pressing keys (n, q ,e). Unfortunately, after choosing an option to not try again, the loop is still running. This is the output copied from the terminal:
Guess the secret number: 1
Guess the secret number: 2
Guess the secret number: 3
Sorry, you are loser!
-------------------------
Would you tray again? Y/N
y
Guess the secret number: 9
The secret number was 9.
You are won!
-------------------------
Would you tray again? Y/N
n
Thank you!
Guess the secret number:
You can see that even after choosing n, it's starting again.
This is the code:
# Guessing Game
def the_func() :
again = 'Would you tray again? Y/N'
scecret_number = 9
guess_count = 0
guess_limit = 3
while guess_count < guess_limit:
guess = int(input("Guess the secret number: "))
guess_count += 1
if guess == scecret_number:
print(f"The secret number was {scecret_number}.\nYou are won!")
break
else:
print("Sorry, you are loser!")
print("-" * int(len(again)), "\n")
print(again)
answer = input(" ")
while True:
if answer.lower() == "y":
the_func()
#elif answer.lower() == "n":
if answer.lower() in {'n', 'q', 'e'}:
print("Thank you!")
#return
break
# else:
# print("Sorry, that was an invalid command!")
the_func()
the_func()
Any help is appreciated.
The problem you are experiencing is known as Recursion. In simple words, the function is getting called repeatedly at the end of it. Here's the example that might clear it more to you:
def foo()
while True:
print('xyz')
break
foo() # Calling the function at the end causes recursion, i.e., infinite looping of the function (unless 'it is returned before the call')
foo()
You might ask, what is the solution?
As said above, you can prevent infinite recursion of a function (if you want to), by implicit returning the function using return.
Your answer is present in your code only. Let me show you where:
while True:
if answer.lower() == "y":
the_func()
#elif answer.lower() == "n":
if answer.lower() in {'n', 'q', 'e'}: # I would use 'elif' here, if the above 'if' statement did not have the calling of the same function. But, here, using 'if' or 'elif' doesn't matter really.
print("Thank you!")
return
#break
#else:
#print("Sorry, that was an invalid command!")
I hope you understood what you are doing wrong, and if you have any doubts, just add a comment and I would help as fast as I can.
Haaa.. DONE, So what we need
while True:
if answer.lower() == "y":
return the_func() ***# <= add return to the_func()***
if answer.lower() in {'n', 'q', 'quit', 'e', 'exit'}:
print("Thank you!")
return
#the_func() #<== mark as comment, and func not will bee looped again the_func()
the_func()
However, #Alex thank you!

Problem with guessing game, player has infinite guesses

I'm trying to build a guessing game in Python. You have a limited number of around 5 guesses/lives and if you run out of them, you will lose the game. (for reference: it uses both random(for random number) and termcolor(for color) modules)
Program:
from termcolor import colored
import random
def lostit():
print(colored("Sorry! You lost!", "red"))
decideto = input("Try again? (yes/no):")
while decideto not in ("yes", "no"):
decideto = input(colored("Invalid response:", "red"))
if decideto is "yes":
guessnum()
elif decideto is "no":
print("Bye, bye.")
def guessnum():
numtoguess = random.randint(1, 10)
print(colored("I've picked a random number from 0 to 9! Guess what it is! You've got 3 hints and 5 guesses!", "green"))
usernum = input(colored("Try to guess it: ", "cyan"))
guesses = 5 # The limit
usertries = 0 # Chances
if guesses >= usertries:
while usernum != numtoguess:
usernum = input(colored("Wrong! Guess again: ", "red"))
usertries += 1 # Tried to make it add until the limit, but doesn't work
elif guesses == usertries:
lostit()
print(colored("Great job! You guessed it!", "green"))
So far, it works when you type in the right number. However, I've experienced problems with the lives/guesses part. I've tried to set a limit to how many tries the player has, however the program seems to ignore this, meaning the player basically has infinite lives. How do I solve this?
print(colored("Sorry! You lost!", "red"))
decideto = input("Try again? (yes/no):")
should likely be indented.
This is also strange. You don't have a variable called lostit, but you have a loop with that variable:
while lostit not in ("yes", "no"):
The logic on your code is simply wrong. See this loop:
while usernum != numtoguess:
usernum = input(colored("Wrong! Guess again: ", "red"))
usertries += 1 # Tried to make it add until the limit, but doesn't work
You are looping while the guess isn't equal to number you selected.
I would reorg. Main function defines the number to guess, how many guesses they get, it loops while two things are true (guess isn't equal to number and guess count is less than total number of guesses). Also note the scope of variables.
from termcolor import colored
import random
def guessnum():
numtoguess = random.randint(1, 10)
print(colored("I've picked a random number from 0 to 9! Guess what it is! You've got 3 hints and 5 guesses!", "green"))
usernum = input(colored("Try to guess it: ", "cyan"))
guesses = 5 # The limit
usertries = 1 # Chances, but they already used a guess
while (usernum != numtoguess) and (guesses >= usertries):
again = input("Try again? (yes/no):")
while again not in ("yes", "no"):
again = input(colored("Invalid response choose yes/no:", "red"))
if decideto is "yes":
usernum = input(colored("Wrong! Guess again: ", "red"))
usertries += 1
elif decideto is "no":
print("Bye, bye.")
return
if usernum == numtoguess:
print(colored("Great job! You guessed it!", "green"))
if guesses >= usertries:
print(colored("Sorry! You lost!", "red"))
Take a look at this section:
if guesses >= usertries:
while usernum != numtoguess:
usernum = input(colored("Wrong! Guess again: ", "red"))
usertries += 1 # Tried to make it add until the limit, but doesn't work
elif guesses == usertries:
lostit()
First of all, your if and elif are not mutually exclusive: let's say guesses is equal to usertries. It will enter to first if (since you use >=) and not the second elif (because it entered the first one). In other words, Elif code is not reachable.
Seoncd, your while loop is inside the if. It keeps running until you guess the right number, and only then compare your guess with "lives". You should substitute the statements to check the lives inside the loop:
while guesses >= usertries:
usernum = input(colored("Wrong! Guess again: ", "red"))
if usernum != numtoguess:
usertries += 1
# User is wrong. We add one to our counter
else
# user is right. Do something to break the loop
# When we reach here, we ended loop: means user lost
lostit()
The logic is: as long as user has guesses, ask him for another number. compare the number: if he is right, do something. else, keep loop.

Why did adding another if statement to the while loop break me out of the loop prematurely?

I'm following a tutorial on while loops that is using a number guessing game as an example. The loop is set to break after three incorrect tries and prints "You lose". I wanted to add another if statement to print after each incorrect guess (Try again), but when I did the loop breaks after the first guess instead of running through all three attempts. Prior to adding the second if statement the program ran through the entire loop correctly.
secret_number = 6
guess_count = 0
guess_limit = 3
while guess_count < guess_limit:
guess = int(input('Guess the secret number! '))
guess_count += 1
if guess == secret_number:
print('...You Won!')
if guess != secret_number:
print('Nope. Try again!')
break
else:
print('...Sorry, you failed.'
As I understand it, the break ignores the if statements and only follows the parameters set by the while command. I don't understand why adding an additional if statement kills the loop after the first attempt.
You misplaced break. You should break from loop when there's a correct guess and retry looping on a wrong guess.
secret_number = 6
guess_count = 0
guess_limit = 3
while guess_count < guess_limit:
guess = int(input('Guess the secret number! '))
guess_count += 1
if guess == secret_number:
print('...You Won!')
break
if guess != secret_number and guess_count != guess_limit:
print('Nope. Try again!')
else:
print('...Sorry, you failed.')
The break statement, like in C, breaks out of the innermost enclosing for or while loop.
https://docs.python.org/3/tutorial/controlflow.html
With:
while guess_count < guess_limit:
....
if guess != secret_number:
print('Nope. Try again!')
break
You're essentially saying: exit the while loop when the guess is wrong.

My randomNumber generator play again acts weird

Now this is pretty beginner version of randomNumber guessing quiz code in Python. Many of you can look at it and make it 4 times shorter and I get it. However the point of this question is that I am unsure of the logic behind this problem.
If you execute this code, it will work just fine - till you get to Play again part.
When you type No - the program as intended quits. Yes starts again.
However if you type Yes first and the game goes one more time and you decide that it is for now I want to quit - this time you have to enter No twice.
And then it quits. I have no idea why. Ideas?
import random
def game(x,y):
con = True
count = 0
ranNum = random.randint(x,y)
while con:
userInput = int(input("Take a guess between these numbers {} and {}: ".format(x,y)))
count +=1
if userInput == ranNum:
print("You got it!")
print("Number of tries: {}.".format(count))
print("Play again?")
while True:
again = input("> ")
if again.lower() == "yes":
game(x,y)
elif again.lower() == "no":
con = False
break
elif userInput < ranNum:
print("Wrong! Try higher!")
elif userInput > ranNum:
print("Wrong! Try lower!")
game(5,10)
The simplest solution: You need a break or return after calling game() when the user answers 'yes'.
This has an unfortunate side effect where each successive game adds another call to the stack. A better design would move the 'play again?' question out of the game function.
The problem is you are running game(x, y) inside itself. Therefore, con is a local variable. Think of it this way: when you type no the first time, it exits out of the game(x, y) that is running inside game(x, y) Therefore, the number of times you have played is the number of times you need to type no. An easier way to do this would be, like #xulfir says, put the "Play again?" loop outside of the function, or you could put exit() instead of using break to just kill the program.

Do Not Display Message While in IF statement

I am going through "Python Programming for the absolute beginner - Third Edition" and inside of that there is a word jumble game. One of the exercises is to offer a hint to the user when they have guessed incorrectly. I have it working however when the hint statement is executed it also prints out the incorrect guess message, and I am stumped as to how to make it not print the incorrect guess message while in the hint.
Here is the code:
guess = input("\nYour guess: ")
wrong_guess_msg = "Sorry that was not correct. Try again."
while guess != correct and guess != "":
print(wrong_guess_msg)
guess = input("Your guess: ")
guess_count += 1
# Ask the user if they would like a hint after 4 wrong guesses
if guess_count == 4:
hint_response = input("Would you like a hint?: ")
if hint_response.lower() == "yes":
print("Here is your hint: {}".format(hint))
else:
guess = input("Your guess: ")
I have tried pulling the if guess_count statement out of the while loop, however it never executes. I tried change the while loop to include a condition where the guess_count != 4 and that sort of worked, meaning I didn't get the incorrect guess when I got the hint, however it exited the program.
Just not really sure what I am missing here.
You could structure the whole loop differently, thereby also eliminating the first prompt before the start of the loop:
wrong_guess_msg = '...'
while True:
guess = input('Your guess: ')
guess_count += 1
if guess == correct:
break
if guess_count == 4:
...
You may argue that this is leaving the while condition unused, but by that, you gain the chance to write your code straight and clean, exactly because you're not forced to put the correctness check right in front of each iteration. Always (if possible) let yourself be guided by how you would spell out an algorithm if explaining it to the next person, rather than by loop mechanics or other technical constraints.

Resources