In python, code clarity and readability is more important than conciseness. If you want concise code, go play code-golf. However, sometimes they align a bit.
This is what I'd make of it, and why I would make the changes:
from random import random
import sys # sys.exit is a different function than the builtin exit. Importing it by name will rename that builtin
# for this script, which should be avoided. Instead, we'll use sys.exit() explicitly.
def get_input(axis): # De-nest this. Flat is better than nested.
while True:
position = input(f"{axis}-Axis: ") # Short, clear variable naming
try:
position = int(position) # this will raise a ValueError when it's not a valid int
if not 0 < position < 8: # same as 1 <= position <= 7. Use whichever one you like.
raise ValueError # ValueError is for invalid values, like out of range.
except ValueError: # Only catch strings which aren't ints and ints out of range. That's the only expected problem here, so other Exceptions should propagate.
print("Please enter an integer between 1 and 7 inclusive.")
continue
return position # Why use else: break? Returning also breaks.
def game(): # Module level function. Avoid deep nesting if you can.
"""
Ripped from https://stackoverflow.com/questions/57813858/iteratively-assigning-variables-in-python/57813901#57813901
""" # Looks like a docstring. Let's position it correctly
a, b, c = (int(7 * random() + 1) for _ in range(3)) # Like @Reinderien said
# I'd prefer meaningful names here as well, but it's short enough that it's not a huge issue, and hard to come up
# with.
print("LOST IN SPACE:\nYou have fifteen hours to find a capsule lost in a SEVEN kilometer cube of space.\n")
# Intro for every new game, instead of only the first game.
for turn in range(5): # Variable names should be meaningful
# 15 turns is enough to figure out a 2^15 = 32768 Kilometer cubed space, since you can solve all axis at the
# same time. 5 turns is plenty. If you go for 15 turns, you can return only 1 distance identifier to make it
# more challenging. Simply continue after printing any of them.
print(f"You have {5 - turn} hours of air left.") # This makes more sense -before- coordinate input
print("Input 3 search coordinates")
d, e, f = [get_input(axis) for axis in "YXZ"]
print(f"Searching at ({d}, {e}, {f})") # f-strings are amazing. Also, explain what you're doing instead of
# dumping variables to the console.
if a > d: # If victorious, this won't print anything anyway:
print("UP")
elif a < d:
print("DOWN")
if b > e:
print("PORT")
elif b < e:
print("STARBOARD")
if c > f:
print("FORWARD")
elif c < f:
print("BACKWARD")
# Lets do all program flow changes at the end of the loop instead of halfway.
if a == d and b == e and c == f: # Old: A == D and B == E and F == C. Why was the order of F == C mixed up
# compared to the other comparisons ?
# Since you just spend an hour searching, you have 1 hour less left. This lets you complete the game with
# 0 hours left - just in time.
print(f"You found the capsule with {4 - turn} hours of air left") # f-strings! Also, explain what you found.
return # Back to main menu. No dedicated function needed.
if turn == 3: # python iteration starts at zero, so the last iteration will be 4, and the warning at 3.
print("Choose your next move carefully... HYPOXIA IMMINENT.")
print(f"Fail, astronaut dead; capsule was at coordinates ({a}, {b}, {c})") # f-string. Also, coordinates are
# generally given as (1, 2, 3) instead of 1.2.3
def main():
# Use this function as main menu.
game() # Run once, then ask for playing again.
while True:
# If we only us a function result once, don't store it in a variable but use it directly. Also, make use of the
# fact that input() prints a message.
if input("Play again? Y/N").lower() == "y":
game()
else:
return 0 # Perhaps another script imports this function to play this game. That only works if we don't call
# sys.exit directly.
if __name__ == "__main__":
# This activates the script when we run it.
sys.exit(main())
Highlights:
- Removed 1-line function of winCondition. It didn't serve a purpose.
- When quitting a game, it's better to return than call another function. We don't need to remember anything anymore.
- Don't nest functions without a really good reason. Good reasons are things like factory functions. If you want them private instead of public, simply prefix them with an underscore.
- We don't need new classes like @Anders InvalidInputError. This error exactly matches the builtin ValueError in purpose, so we use that instead. (his variable naming point is good, even if I chose to not include it.)
- Separation of concerns. The game() function runs just a single game. The main() (menu) starts games. The get_input() earns it's keep with it's error checking.
- Removed end() function. It did the work of the main menu, so that's where it's code went.