Created
March 19, 2026 16:52
-
-
Save basperheim/11e3ac111e42d6bd1caa1677c8c723e1 to your computer and use it in GitHub Desktop.
Math Quiz Python Script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| from __future__ import annotations | |
| import random | |
| import sys | |
| from dataclasses import dataclass | |
| from decimal import Decimal, InvalidOperation, ROUND_HALF_UP | |
| GREEN = "\033[92m" | |
| RED = "\033[91m" | |
| CYAN = "\033[96m" | |
| YELLOW = "\033[93m" | |
| RESET = "\033[0m" | |
| QUESTION_COUNT = 10 | |
| DECIMAL_PLACES = Decimal("0.01") | |
| @dataclass(frozen=True) | |
| class Question: | |
| prompt: str | |
| answer: Decimal | |
| def clamp_difficulty(value: int) -> int: | |
| return max(1, min(10, value)) | |
| def classify_answer(user_answer: Decimal, expected_answer: Decimal) -> tuple[str, Decimal]: | |
| if user_answer == expected_answer: | |
| return ("exact", Decimal("1.0")) | |
| if expected_answer == expected_answer.to_integral(): | |
| return ("wrong", Decimal("0.0")) | |
| rounded_to_tenth = expected_answer.quantize(Decimal("0.1"), rounding=ROUND_HALF_UP) | |
| if expected_answer == rounded_to_tenth: | |
| if abs(user_answer - expected_answer) <= Decimal("0.05"): | |
| return ("close", Decimal("0.5")) | |
| return ("wrong", Decimal("0.0")) | |
| if abs(user_answer - expected_answer) <= Decimal("0.01"): | |
| return ("close", Decimal("0.5")) | |
| return ("wrong", Decimal("0.0")) | |
| def score_str(value: Decimal) -> str: | |
| if value == value.to_integral(): | |
| return str(value.to_integral()) | |
| return format(value, "f").rstrip("0").rstrip(".") | |
| def parse_difficulty(argv: list[str]) -> int: | |
| if len(argv) < 2: | |
| return 5 | |
| raw_value = argv[1] | |
| try: | |
| parsed = int(raw_value) | |
| except ValueError: | |
| print( | |
| f"{YELLOW}Invalid difficulty '{raw_value}'. Using default difficulty 5.{RESET}" | |
| ) | |
| return 5 | |
| if not 1 <= parsed <= 10: | |
| print( | |
| f"{YELLOW}Difficulty must be between 1 and 10. " | |
| f"Using clamped value {clamp_difficulty(parsed)}.{RESET}" | |
| ) | |
| return clamp_difficulty(parsed) | |
| def decimal_probability(difficulty: int) -> float: | |
| if difficulty <= 2: | |
| return 0.02 | |
| if difficulty <= 5: | |
| return 0.10 | |
| if difficulty <= 7: | |
| return 0.25 | |
| return 0.40 | |
| def random_friendly_decimal(difficulty: int) -> Decimal: | |
| if difficulty <= 5: | |
| return random.choice([ | |
| Decimal("0.1"), | |
| Decimal("0.2"), | |
| Decimal("0.5"), | |
| ]) | |
| if difficulty <= 7: | |
| return random.choice([ | |
| Decimal("0.1"), Decimal("0.2"), Decimal("0.25"), Decimal("0.3"), | |
| Decimal("0.4"), Decimal("0.5"), Decimal("0.6"), Decimal("0.75"), | |
| Decimal("0.8"), Decimal("0.9"), | |
| ]) | |
| cents = Decimal(random.randint(1, 99)) / Decimal(100) | |
| return quantize_2(cents) | |
| def random_add_sub_number( | |
| difficulty: int, | |
| min_whole: int, | |
| max_whole: int, | |
| ) -> Decimal: | |
| whole = Decimal(random.randint(min_whole, max_whole)) | |
| if random.random() > decimal_probability(difficulty): | |
| return whole | |
| return quantize_2(whole + random_friendly_decimal(difficulty)) | |
| def quantize_2(value: Decimal) -> Decimal: | |
| return value.quantize(DECIMAL_PLACES, rounding=ROUND_HALF_UP) | |
| def decimal_str(value: Decimal) -> str: | |
| normalized = quantize_2(value) | |
| if normalized == normalized.to_integral(): | |
| return str(normalized.to_integral()) | |
| text = format(normalized, "f") | |
| text = text.rstrip("0").rstrip(".") | |
| return text | |
| def difficulty_ranges(difficulty: int) -> dict[str, tuple[int, int]]: | |
| add_sub_max = 10 + (difficulty * 50) | |
| mult_max = 3 + (difficulty * 2) | |
| div_max = 3 + (difficulty * 2) | |
| return { | |
| "add_sub": (0, add_sub_max), | |
| "mult": (1, mult_max), | |
| "div": (1, div_max), | |
| } | |
| def generate_addition(difficulty: int) -> Question: | |
| ranges = difficulty_ranges(difficulty) | |
| min_value, max_value = ranges["add_sub"] | |
| a = random_add_sub_number(difficulty, min_value, max_value) | |
| b = random_add_sub_number(difficulty, min_value, max_value) | |
| answer = quantize_2(a + b) | |
| return Question(prompt=f"{decimal_str(a)} + {decimal_str(b)} = ", answer=answer) | |
| def generate_subtraction(difficulty: int) -> Question: | |
| ranges = difficulty_ranges(difficulty) | |
| min_value, max_value = ranges["add_sub"] | |
| if difficulty <= 4 and random.random() < 0.7: | |
| a = Decimal(random.randint(min_value, max_value)) | |
| b = Decimal(random.randint(min_value, int(a))) | |
| else: | |
| a = random_add_sub_number(difficulty, min_value, max_value) | |
| b = random_add_sub_number(difficulty, min_value, max_value) | |
| if a < b: | |
| a, b = b, a | |
| answer = quantize_2(a - b) | |
| return Question( | |
| prompt=f"{decimal_str(a)} - {decimal_str(b)} = ", | |
| answer=answer, | |
| ) | |
| def generate_multiplication(difficulty: int) -> Question: | |
| ranges = difficulty_ranges(difficulty) | |
| min_value, max_value = ranges["mult"] | |
| a = Decimal(random.randint(min_value, max_value)) | |
| b = Decimal(random.randint(min_value, max_value)) | |
| answer = quantize_2(a * b) | |
| return Question( | |
| prompt=f"{decimal_str(a)} * {decimal_str(b)} = ", | |
| answer=answer, | |
| ) | |
| def generate_division(difficulty: int) -> Question: | |
| ranges = difficulty_ranges(difficulty) | |
| min_value, max_value = ranges["div"] | |
| divisor = random.randint(max(1, min_value), max_value) | |
| if difficulty <= 5 or random.random() < 0.75: | |
| quotient = Decimal(random.randint(1, max_value)) | |
| dividend = divisor * int(quotient) | |
| else: | |
| if difficulty <= 7 and random.random() < 0.7: | |
| # force .5 answers with even divisor so dividend stays whole | |
| divisor *= 2 | |
| quotient = Decimal(random.randint(1, max_value)) + Decimal("0.5") | |
| dividend = int(Decimal(divisor) * quotient) | |
| else: | |
| quotient = Decimal(random.randint(1, max_value)) | |
| dividend = divisor * int(quotient) | |
| answer = quantize_2(Decimal(dividend) / Decimal(divisor)) | |
| return Question( | |
| prompt=f"{dividend} / {divisor} = ", | |
| answer=answer, | |
| ) | |
| def generate_question(difficulty: int) -> Question: | |
| if difficulty <= 3: | |
| generators = [ | |
| generate_addition, | |
| generate_addition, | |
| generate_addition, | |
| generate_subtraction, | |
| generate_subtraction, | |
| generate_multiplication, | |
| generate_division, | |
| ] | |
| elif difficulty <= 6: | |
| generators = [ | |
| generate_addition, | |
| generate_addition, | |
| generate_subtraction, | |
| generate_subtraction, | |
| generate_multiplication, | |
| generate_division, | |
| ] | |
| else: | |
| generators = [ | |
| generate_addition, | |
| generate_subtraction, | |
| generate_multiplication, | |
| generate_division, | |
| ] | |
| return random.choice(generators)(difficulty) | |
| def parse_user_answer(raw_input: str) -> Decimal: | |
| cleaned = raw_input.strip() | |
| return quantize_2(Decimal(cleaned)) | |
| def main() -> None: | |
| difficulty = parse_difficulty(sys.argv) | |
| score = Decimal("0.0") | |
| print(f"{CYAN}Math Quiz - 10 Questions{RESET}") | |
| print(f"{CYAN}Difficulty: {difficulty}{RESET}") | |
| print() | |
| for question_number in range(1, QUESTION_COUNT + 1): | |
| question = generate_question(difficulty) | |
| print(f"{CYAN}Question {question_number}/{QUESTION_COUNT}{RESET}") | |
| while True: | |
| raw_answer = input(question.prompt) | |
| try: | |
| user_answer = parse_user_answer(raw_answer) | |
| break | |
| except InvalidOperation: | |
| print(f"{YELLOW}Please enter a valid number.{RESET}") | |
| result, points = classify_answer(user_answer, question.answer) | |
| score += points | |
| if result == "exact": | |
| print(f"{GREEN}Correct!{RESET}") | |
| elif result == "close": | |
| print( | |
| f"{YELLOW}Close.{RESET} " | |
| f"You get half a point. " | |
| f"The exact answer was {decimal_str(question.answer)}." | |
| ) | |
| else: | |
| print( | |
| f"{RED}Incorrect.{RESET} " | |
| f"The correct answer was {decimal_str(question.answer)}." | |
| ) | |
| print() | |
| percentage = (score / Decimal(QUESTION_COUNT)) * Decimal("100") | |
| print(f"{CYAN}Quiz Complete{RESET}") | |
| print(f"Difficulty level: {difficulty}") | |
| print(f"Score: {score_str(score)}/{QUESTION_COUNT}") | |
| print(f"Percentage: {percentage:.0f}%") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment