3
\$\begingroup\$

Coming from a C/C++ background, this was my first python code I wrote to grasp a few basic concepts. The game involves giving random arithmetic operations to the users and scoring their responses. Player has 3 lives i.e he can mark 3 wrong answers at most. I've tried to time the game i.e the player gets 60 seconds to solve as many questions as possible.

Any suggestions and improvements are welcome.

import random
import time


# Function to generate expressions
from typing import List


def generate_expression(no_of_operators):
    operands = []
    operators = []
    operations = ['+', '-', '*', '/']
    expression = []
    operands_count = 0
    operators_count = 0
    for i in range(0, no_of_operators):
        operands.append(random.randint(0, 20))
    for i in range(0, no_of_operators - 1):
        operators.append((random.choice(operations)))
    for i in range(0, len(operators) + len(operands)):
        if i % 2 == 0:
            expression.append(operands[operands_count])
            operands_count += 1
        else:
            expression.append(operators[operators_count])
            operators_count += 1
    expression = ''.join(str(x) for x in expression)
    return expression


# Function to calculate the solution
def result(expression):
    return (int(eval(expression)))


# Function to evaluate if the answer is right
def evaluate(solution, user_solution):
    if solution == user_solution:
        return True
    else:
        return False



# Display Message

print("""Welcome to the maths game !!!
-----------------------------
Test your basic arithematic skills by playing this simple game. With every 5 correct answers, the level increase
increasing the difficulty of the questions.
Remember : 
----------
        1. Write only the integral part of the answer 
        2. Operator precedence applies
        3. You have 3 lives.
        4. Total of 60 seconds will be provided.
        5. The timer starts after the first answer is entered """)
input("Are you ready ?? Press any key to begin ! ")

# Variables on which the game operates
score = 0
level = 1
lives = 3
start = time.time()
finish_time = time.time() + 60  # for the timed mode, 60 seconds are needed

# While loop to drive the game
while lives != 0 and time.time() < finish_time:
    # Increase the level of difficulty every 5 questions.
    if score != 0 and score % 5 == 0:
        level = level + 1
    print("LEVEL : ", level)
    no_of_operands = level + 1
    question_expression = generate_expression(no_of_operands)
    print(question_expression, end='')
    # Checking for any divide by zero or numerical errors that may show up
    correct_answer = 0
    try:
        correct_answer = result(question_expression)
    except:
        print("OOPS ! I messed up ! Lets do it again !")
        continue
    answer = int(input(" = "))

    if evaluate(correct_answer, answer):
        print("CORRECT ! ", end='')
        score = score + 1
        print("SCORE = ", score, "LIVES = ", lives)
    else:
        print("WRONG ! ", end='')
        lives = lives - 1
        print("SCORE = ", score, "LIVES = ", lives)
print("GAME OVER !!!")
print("Maximum Level = ", level, "SCORE = ", score)
\$\endgroup\$
1
\$\begingroup\$

Welcome to Code Review.

The type List, imported by from typing import List is never used. You can delete this statement.

The function def generate_expression(no_of_operators): is a little verbose and complex. You don't need to create separate lists for operands and operators, and then try to combine them afterwards. Instead, you could simply create the expression terms directly in a loop:

def generate_expression(no_of_operators):
    operations = ['+', '-', '*', '/']
    expression = []

    expression.append(random.randint(0, 20))

    for _ in range(no_of_operators):
        expression.append(random.choice(operations))
        expression.append(random.randint(0, 20))

    expression = ''.join(str(term) for term in expression)
    return expression

Here, I've started the expression with a random number, and then, in pairs, added an operator and another random number. When a loop index is not used, _ is commonly used for that variable. When the beginning of a range is 0, that is usually omitted as well.

Adding a space between the operands and operators may be a little more user friendly:

    expression = ' '.join(str(term) for term in expression)

Parenthesis are not need around the return value in return (int(eval(expression))). Instead, simply write: return int(eval(expression))

The evaluate(solution, user_solution) function can be shortened, by using the test condition as the return value.

def evaluate(solution, user_solution):
    return solution == user_solution

Considering how simple the function is, you might consider removing it, and just using the following in your main program:

    if answer == correct_answer:

The timer starts when the first problem is displayed, not after the first answer is entered. Then again, the timer may expire after the last answer has been displayed, before the user types in their answer, and they'll still get credit for it, so the misleading instruction information wrong twice in the same way, and in this case two wrongs sort of make a right. :-)

The following statement is unnecessary. It is immediately overwritten, or the code skips to top of the loop.

    correct_answer = 0

There is a logic error here:

while lives != 0 and time.time() < finish_time:
    # Increase the level of difficulty every 5 questions.
    if score != 0 and score % 5 == 0:
        level = level + 1
    #...
    try:
        correct_answer = result(question_expression)
    except:
        print("OOPS ! I messed up ! Lets do it again !")
        continue
    #...

If the user's score is a multiple of 5, the difficulty increases. Then, if the question_expression results in a division by 0, you continue from the top of the loop, and the difficulty is increase again! If you are unlucky, it can happen many times in a row. A simple fix is moving the difficulty increase code into the score increase code at the end of the loop:

    if answer == correct_answer:
        print("CORRECT ! ", end='')
        score = score + 1
        print("SCORE = ", score, "LIVES = ", lives)
        if score % 5 == 0:
            level = level + 1
            print("LEVEL : ", level)
    else:
        #...
\$\endgroup\$
  • \$\begingroup\$ Thanks for a very descriptive response. I will surely work on your suggested improvements. How can I fix the timer so that it stops after the first answer has been input and terminates the loop right after the time is exceeded ? \$\endgroup\$ – N_H1922 Aug 29 '18 at 18:37
  • \$\begingroup\$ As a beginner Python programmer, I'd stay away from trying to stop the user input with a timer. The question has been asked many times, with varying levels of success: see here, or here, or here, or here. Depends on Windows or Unix, version of Python, etc. \$\endgroup\$ – AJNeufeld Aug 29 '18 at 19:10
2
\$\begingroup\$

Quick comment: If you have code which is supposed to run when you call the script (in your case its the "run the game" part of the code), its good practice to put it in a main() function and call the main function rather than keep all that code at the highest level of the file. At the bottom of the file, it would look something like this:

def main():
    functions_to_play_math_game()

if __name__ == "__main__":
    main()

When a python script runs, it gets a __name__ attribute depending on how it was called. If it was run through the command line (with $ python my_script.py), the __name__ attribute gets set to "__main__". So the second part of my code sample checks if the __name__ attribute indicates the script is being called to run, and if so, executes the code inside main().

The reason this is important is because when you import a module in python, it runs all the code inside that file. Now, if the file only contains function definitions, nothing out of the ordinary happens. But if you imported your file into another file, at the import my_file line your game would start running because there is no safeguard to prevent it from going. The catch is that when being imported, __name__ is not set to __main__ like it is when you call the script from a command line. So, if you have this safeguard at the bottom of your code, it wont run when its imported.

\$\endgroup\$

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.