Skip to content

Instantly share code, notes, and snippets.

@basperheim
Created March 19, 2026 16:52
Show Gist options
  • Select an option

  • Save basperheim/11e3ac111e42d6bd1caa1677c8c723e1 to your computer and use it in GitHub Desktop.

Select an option

Save basperheim/11e3ac111e42d6bd1caa1677c8c723e1 to your computer and use it in GitHub Desktop.
Math Quiz Python Script
#!/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