3
\$\begingroup\$

This code asks the user how many multiplication questions they want to answer and then asks the requested amount of questions. When all of the questions are answered, it prints the percentage the user got correct of the amount answered.

This code works as intended. Is there any way I could make it more efficient? Do you have any Suggestions?

import random alreadyAsked = [] questionNumber = 1 numCorrect = 0 def generateNumbers(): num1 = random.randint(-12,12) num2 = random.randint(-12,12) #Checks if numbers have already been asked. while [num1,num2] in alreadyAsked: num1 = random.randint(-12,12) num2 = random.randint(-12,12) return num1, num2 print("Welcome to Multiplication Game") numQuestions = int(input("How many Questions would you like to answer? ")) for _ in range(numQuestions): genNum = generateNumbers() alreadyAsked.append(genNum) userAnswer = int(input(f'Question #{questionNumber}: {genNum[0]} x {genNum[1]}: ')) answer = genNum[0] * genNum[1] if userAnswer == answer: print("Correct!") numCorrect += 1 else: print("Incorrect!") questionNumber += 1 if numQuestions > 0: percentCorrect = round((numCorrect / numQuestions) * 100, 2) if percentCorrect.is_integer(): percentCorrect = int(percentCorrect) print(f'You got {percentCorrect}%!') else: print("You cannot choose to answer 0 Questions.") 
\$\endgroup\$
2
  • \$\begingroup\$(1/2) Efficiency doesn't matter in such simple problems – if a program plays question-and-answer with a human, the processing time less than about 1/2 to 1 second is negligible, the program's reaction is perceived as instantaneous. It takes much longer for us, humans, to see and understand the prompt and prepare and type the answer. In such cases the general clarity and maintainability (data structure, code structure, naming, documentation) is much more important than the processing time.\$\endgroup\$
    – CiaPan
    CommentedJan 16 at 7:09
  • \$\begingroup\$(2/2) The most annoying and irritating response time is from circa 3 or 5s, up to some 10 or 15 sec. It makes a noticeable break in our work flow, although we don't ususlly loose our interest yet. Fixing those delays is most important for the end user. Suspensions longer than that almost surely make user to switch to some YouTube, TikTok or just walk away to make a cup of coffee. Fixing these is most important for the the end user's employer.\$\endgroup\$
    – CiaPan
    CommentedJan 16 at 7:09

3 Answers 3

7
\$\begingroup\$

To tackle one aspect of your program, if you wish to generate unique (previously unused) random permutations of two numbers between -12 to 12 you may wish to generate all such permutations upfront in your program as the number of options isn't overwhelming.

number_pairs = list(itertools.permutations(range(-12, 13), 2)) 

Then you can shuffle these with random.shuffle:

random.shuffle(number_pairs) 

At this point, you can just pop values off of this list to get unique, non-repeating random number pairs, and that operation runs in constant time.

This is as opposed to the indeterminate runtime complexity of picking random pairs until you get one that hasn't previously been picked. It might be the first one you try, or it might take hundreds or thousands of tries. The extra work upfront removes this uncertainty from each selection.

The original approach scales especially poorly because as the number of previously selected pairs increases, so too the amount of work to check against that set increases and the likelihood of selecting a previously unselected pair decreases, meaning a likely larger number of more expensive operations.

\$\endgroup\$
    5
    \$\begingroup\$

    Note that in my response I will be following the PEP8 naming conventions and so I might be referencing functions and variables that have been renamed from your original names.

    Bugs

    1. You keep track of the number pairs being generated by storing in a list named already_asked a tuple. But to see if a pair has already been generated, you are searching the list for a list of integers rather than a tuple of integers. Your search can never succeed.
    2. In a couple of places you ask the user to enter an integer value. But if the user enters an invalid numeric, e.g. 'abc', your code will raise an exception. I would suggest that you create a function call input_integer that will prompt the user with a user-supplied prompt until a valid value is entered. This function would profit by taking an optional range of integers (a tuple or list of two integers specifying a minimum and maximum value for the user's input).
    3. If the user enters 0 for the number of questions to be asked, you put out an error message that the user cannot enter that value, but then you terminate the script. Instead use the input_integer function mentioned above to ensure that the user supplies an answer that is at least 1 with some reasonable upper boundary.
    4. As pointed out by STerliakov, there are only 25 * 25 = 625 possible pairs of randomly-generated numbers if the range for a random number includes 25 distinct values. So if the user wants to be asked more than 625 questions, the code will loop forever on the 626th question trying to generate a pair of numbers that haven't been seen before.

    Choose More Meaningful Names for Functions and Variables

    Function generateNumbers specifically generates a pair of integers and that this is so is explicitly assumed by the your code that calls the function. So better would be to rename it to something more descriptive, such as generate_integer_pair. You are also assigning the returned tuple of integers to a variable named genNum. This name could also be more descriptive, for example integer_pair.

    Don't Repeat Yourself (DRY)

    I don't believe you need to create a separate function that returns an integer within a specific range so that function generateNumbers does not have multiple calls to random.randint(-12, 12). I think it is sufficient to simply define "constants" such as FROM_INT, TO_INT = -12, 12 and use those constants instead of -12 and 12. This allows you to modify the range in a single place. But the code for this function can be simplified so that code to get the random pair does not need to be duplicated when a pair that has already been used before is detected.

    A Small Inefficiency

    In your function generateNumbers you have expressions such as genNum[0] specified multiple times. While this is not terribly inefficient, you could unpack the randomly generated tuple into a pair of simple variables once and then use those variables. It also makes it clear that generateNumbers is returning a tuple of two integers.

    A Larger Inefficiency

    Sequentially searching alreadyAsked to see if a random pair has already been used is less efficient than storing the pair as a tuple in a set instance.

    Separation of Concerns

    We would like function generateNumbers to generate a pair of integers that has not been previously generated yet you are relying on a client of this function to store all previously generated pairs. generateNumbers should be the one concerned with ensuring that generated pairs are remembered.

    Make the Game "Importable"

    Place the main logic in a function, for example multiplication_game, which can now be imported from another script.

    Docstrings

    Add these for the module and functions.

    Type Hinting

    Use this for function definitions.

    Putting It All Together

    """This module provides a multiplication game.""" import random FROM_INT, TO_INT = -12, 12 # Define the range once! range_size = TO_INT - FROM_INT + 1 number_of_possible_pairs = range_size * range_size question_number = 1 num_correct = 0 already_asked = set() def generate_integer_pair() -> tuple[int]: """Generate a pair of integers in the closed interval [FROM_INT, TO_INT].""" #Checks if numbers have already been asked. while True: integer_pair = random.randint(FROM_INT, TO_INT), random.randint(FROM_INT, TO_INT) if integer_pair not in already_asked: break already_asked.add(integer_pair) return integer_pair def input_integer(prompt: str, valid_range=None) -> int: """Prompt the user for an integer value and optionally check to see if it within a certain range.""" while True: try: user_answer = int(input(prompt)) except ValueError: # Invalid input print('You must enter a valid integer value!') else: if valid_range is not None: min_value, max_value = valid_range if min_value <= user_answer <= max_value: return user_answer else: print(f'Your response must be an integer in the range [{min_value}, {max_value}]') else: return user_answer def multiplication_game(): """This is the game, which can be imported.""" global num_correct print("Welcome to the Multiplication Game!") num_questions = input_integer("How many Questions would you like to answer? ", (1, number_of_possible_pairs)) for question_number in range(1, num_questions + 1): integer_pair = generate_integer_pair() int1, int2 = integer_pair user_answer = input_integer(f'Question #{question_number}: {int1} x {int2}: ') answer = int1 * int2 if user_answer == answer: print("Correct!") num_correct += 1 else: print("Incorrect!") # An alternate approach suggested by Stef would be: # percent_correct = f'{num_correct*100/num_questions:.2f}'.strip('0').strip('.') percent_correct = round((num_correct / num_questions) * 100, 2) if percent_correct.is_integer(): percent_correct = int(percent_correct) print(f'Your score: {percent_correct}%') if __name__ == '__main__': multiplication_game() 

    Questions for Tomorrow

    Python supports object-oriented programming. Where could it be put to good use? Can we create reusable classes that might be used in other applications? For example, how might we better encapsulate variable already_asked with the function generate_integer_pair that uses it?

    \$\endgroup\$
    16
    • \$\begingroup\$One more bug you haven't caught: entering 1000 for num_questions will cause an infinite loop in generate_integer_pair since all combinations will be exhausted earlier (there are only 625 options).\$\endgroup\$CommentedNov 19, 2024 at 17:26
    • \$\begingroup\$@STerliakov Good catch! In my answer I said to use a reasonable upper limit. Clearly 1000 would unreasonable. I have added this to my answer.\$\endgroup\$
      – Booboo
      CommentedNov 19, 2024 at 18:14
    • \$\begingroup\$I suggest simplifying the percentage display to something like ratio=num_correct/num_questions;print(f'Your score: {ratio:.2%}')\$\endgroup\$
      – Stef
      CommentedDec 16, 2024 at 11:47
    • \$\begingroup\$@stef The reason I didn't do it was so that when the user gets half the questions correct the OP would prefer to display '50%' instead of '50.00%' (see the OP's original code). If neither representation were preferred over the other by the OP (or if '50.00%' were the preferred display), then your change would certainly be simpler.\$\endgroup\$
      – Booboo
      CommentedDec 16, 2024 at 14:08
    • \$\begingroup\$@Booboo So the OP wants "50%" rather than "50.00%", but "99.50%" rather than "99.5%"?\$\endgroup\$
      – Stef
      CommentedDec 16, 2024 at 15:32
    4
    \$\begingroup\$

    Layout

    Move the function to the top after the import line. Having it in the middle of the code interrupts the natural flow of the code (from a human readability standpoint).

    DRY

    This code is repeated 4 times:

    random.randint(-12,12) 

    You could move it to its own function. This would make it easier to maintain should you ever want to change the range of values. You could even allow the user an option to enter a range.

    Naming

    The PEP 8 style guide recommends using snake_case for function and variable names:

    generate_numbers already_asked 

    Documentation

    Add a doctring at the top of the file to summarize the purpose of the code:

    """ This code asks the user how many multiplication questions they want to answer and then asks the requested amount of questions. When all of the questions are answered, it prints the percentage you got correct of the amount you answered. """ 

    Add a docstring to describe the function, especially what is returned.

    Extra code

    It is not clear to me why you need the following 2 lines:

    if percentCorrect.is_integer(): percentCorrect = int(percentCorrect) 

    The code seems to work without them when I run it. If you know of a condition where the code is needed, I recommend adding a comment. Otherwise, delete the code.

    Input checking

    Consider using pyinputplus to catch unexpected input (typos and such).

    Potential bug

    Here is the output of one run:

    Welcome to Multiplication Game How many Questions would you like to answer? 2 Question #1: 9 x 0: 0 Correct! Question #2: 9 x 0: 0 Correct! You got 100.0%! 

    It asked the same question twice (9 x 0). I think you attempt to avoid this by keeping track in the alreadyAsked variable. If you agree it is a bug, it will be easier to reproduce and debug with a smaller range. For example, instead of -12,12, try 1,3.

    \$\endgroup\$
    4
    • \$\begingroup\$If the user scores 5 out of 10 questions correct, then the result printed will be 50.0% without the casting to an int. The OP prefers to print '50%' in this case. This is not a bug; it's a feature.\$\endgroup\$
      – Booboo
      CommentedNov 19, 2024 at 0:17
    • 1
      \$\begingroup\$@Booboo: Thanks for providing a scenario. I think it is still worth adding a comment to the code since it might not be obvious to future maintainers of the code.\$\endgroup\$
      – toolic
      CommentedNov 19, 2024 at 0:24
    • 1
      \$\begingroup\$@toolic Future maintainers, including yourself! If you come back to this piece of code in 2 years time, will you still remember why you put it there? I probably wouldn't.\$\endgroup\$
      – Dnomyar96
      CommentedNov 19, 2024 at 15:16
    • \$\begingroup\$The display should probably be simplified using all the formatting options provided by f-strings, rather than transforming a float into an int to remove a .0 in the display. In fact, f'Your score: {num_correct/num_questions:.2%}' would avoid using round and int\$\endgroup\$
      – Stef
      CommentedDec 16, 2024 at 11:50

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.