2
\$\begingroup\$

I'm a self-taught programmer starting out with Python and my latest project is this game: It runs in the console, displays an equation, takes the user's answer, and increases your score if it's correct. Every level has 5 questions, and the operands get bigger each level. At the end of each level. It asks if you want to continue or quit.

I managed to get it down to 41 SLOC but I want to know if there are ways I don't know about to make it even shorter.

#   ----    IMPORTS    ----
from random     import randint, choice
from re         import search
from subprocess import run

#   ----    VARIABLES    ----
multis  = [ -1, 1 ]                     # "Multipliers"
ops     = [ "+", "-", "*", "/" ]        # "Operators"
score   = 0
level   = 1

#   ----    FUNCTIONS    ----
def setQuestion():
    global answer
    global userInput

    # Set the two operands and operator.
    oper1       = randint( (level * 5), (level * 10) ) * choice( multis )       # "Operand 1"
    oper2       = randint( 1, abs(oper1) ) * choice( multis )                  # "Operand 2"
    opsCheck    = randint( 0, 3 )

    # If the operator is division, subtract the modulus from {oper1} so that
    # the quotient is an integer.
    if( opsCheck == 3 ):
        if( oper1 < 0 ):
            oper1 -= ( abs(oper1) % oper2 ) * -1
        else:
            oper1 -= oper1 % oper2 

    # Display the question and get the answer.
    userInput = input( (" {} {} {} = ?\n A: ").format(oper1, ops[opsCheck], oper2) )

    # Set global {answer} to the solution.
    answer = eval( ("{}{}{}").format(oper1, ops[opsCheck], oper2) )

def runLevel():
    global score

    # Each level has 5 questions
    for i in range( 5 ):
        # Clear the screen each level (Unix-Like).
        run( "clear", shell = True )

        # Print the header and run the question.
        print( ("\t----    LEVEL {}:    ----\n").format(level) )
        setQuestion()

        # If correct, increase score. If not, display correct answer.
        if( int(userInput) == answer ):
            print( "\n Correct!" )
            score += 1
        else: print( ("\n Incorrect. Sorry.\n The answer was {}.").format(answer) )

        # Wait for user input to move on to the next question.
        input( "\n (Press any key to advance)" )

#   ----    MAIN    ----
# Program will run continuously
while( True ):
    # Go through one level
    runLevel()

    # Clear and display "Level Complete" Screen and player score.
    run( "clear", shell = True )
    print( ("\t---   LEVEL {} COMPLETE!   ---").format(level) )
    userInput = input( ("\n Your score is {}\n Continue? (y/n) ").format(score) )

    # If user enters "y" or "Y", increase global {level} by one and restart.
    if( search("y|Y", userInput) ):
        level += 1
        continue
    # If user does not enter "y" or "Y", clear screen and terminate.
    run( "clear", shell = True )
    exit()
```
\$\endgroup\$
1
  • \$\begingroup\$ Maybe: oper1 -= -(abs(oper1) % oper2) if (oper1 < 0) else (oper1 % oper2) after the if( opsCheck == 3 ): line? these kind of if-else statements can be written in a single line. But it is pretty ugly... \$\endgroup\$
    – simre
    May 15 at 20:09

1 Answer 1

1
\$\begingroup\$

I want to know if there are ways I don't know about to make it even shorter.

Shorter isn't always better, and this is not code golf. Strive for good code, and the short code will come naturally as a side-effect.

Your IMPORTS and VARIABLES comments should be deleted.

Your multis are really just signs. But you don't need to random.choice on these; you can get a sign directly from randrange.

setQuestion should first of all be set_question by PEP8, and also doesn't "set the question"; it "asks the question" so should be renamed.

score and level should not be global.

opsCheck is not a "check", it's an operator index. But you don't need to choose an index; all you need to do is choose a key of a dictionary where the keys are your existing ops values and the values are functions from operators.

The modulus magic is nasty. I would sooner choose two operands, find their quotient, and then multiply back the quotient to get a perfect multiple.

Get into the habit of using f-strings instead of format() calls when they're suitable (which is frequent).

Never use eval. There's basically never a use case that calls for it, particularly in beginner code.

Your clear and your bonus newlines sprinkled through the output are harming, not helping. Good console UI design shows output in well-organised paragraphs, and those paragraphs don't vanish between levels or the end of the program. Related: the question prompt should not be split across two different lines.

Pressing a key to continue is really only helpful if there's a flood of text risking exceeding the buffer height, and that isn't the case here; so don't do it.

Consider making [y] the default answer for whether the user wants to continue; convention is to show the default in brackets and assume that default if the user just presses enter.

Regexes are not appropriate here. You can just check the first letter of the response.

Avoid exit(); write a main() function and return from it when you're done.

Suggested

from itertools import count
from random import choice, randint, randrange
import operator

ops = {
    "+": operator.add,
    "-": operator.sub,
    "*": operator.mul,
    "/": operator.floordiv,
}


def rand_sign() -> int:
    return randrange(-1, 2, 2)


def ask_question(level: int) -> tuple[
    int,  # user answer
    int,  # actual answer
]:
    x = randint(a=level*5, b=level*10) * rand_sign()
    y = randint(a=1, b=abs(x)) * rand_sign()
    op = choice(tuple(ops.keys()))

    if op == '/':
        answer = x//y
        x = answer*y
    else:
        answer = ops[op](x, y)

    user_input = int(input(f"{x} {op} {y} = "))
    return user_input, answer


def run_level(level: int, n_questions: int = 5) -> int:
    print(f"----    LEVEL {level}:    ----")
    score_change = 0
    for _ in range(n_questions):
        user_answer, actual_answer = ask_question(level)

        if user_answer == actual_answer:
            print("Correct!")
            score_change += 1
        else:
            print(f"Incorrect. Sorry. The answer was {actual_answer}.")

    return score_change


def main() -> None:
    score = 0
    for level in count(1):
        score += run_level(level)

        print(
            f"---   LEVEL {level} COMPLETE!   ---"
            f"\n Your score is {score}"
        )

        user_input = input('Continue [y]/n? ').lower()
        if user_input.startswith('n'):
            break
        print()


if __name__ == '__main__':
    main()

Output

----    LEVEL 1:    ----
8 / 8 = 1
Correct!
7 * -6 = 99
Incorrect. Sorry. The answer was -42.
7 * 4 = 28
Correct!
9 / -3 = -3
Correct!
-8 / -2 = 4
Correct!
---   LEVEL 1 COMPLETE!   ---
 Your score is 4
Continue [y]/n? y

----    LEVEL 2:    ----
-12 + 4 = -8
Correct!
-11 - 9 = 
...

^ This demonstrates what I mean when I say "paragraphs". All of level 1 is grouped together, and the only time there is a blank line is between levels.

\$\endgroup\$
2
  • \$\begingroup\$ If I may ask one follow-up question: What's the purpose of declaring variables a and b in randint( a=level*5, b=level*10)? \$\endgroup\$
    – gZee
    May 15 at 23:08
  • \$\begingroup\$ @gZee Those are the named parameters of randint, not really variables. It's often useful to show exactly which parameters are being set. \$\endgroup\$
    – Reinderien
    May 15 at 23:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.