"""
Multiplication Mastery (Console App)
-----------------------------------
A full-featured multiplication practice tool:
- Tables generator
- Practice quiz (untimed or timed)
- Difficulty levels
- Score + accuracy tracking
- Mistake review
- Save session results to a file
Educational purpose only.
"""
from __future__ import annotations
import random
import time
from dataclasses import dataclass, asdict
from typing import List, Tuple, Optional
from pathlib import Path
import json
# -----------------------------
# Data Models
# -----------------------------
@dataclass
class Question:
a: int
b: int
user_answer: Optional[int] = None
correct_answer: Optional[int] = None
is_correct: Optional[bool] = None
time_taken_s: Optional[float] = None
def finalize(self, user_answer: int, time_taken_s: float) -> None:
self.user_answer = user_answer
self.correct_answer = self.a * self.b
self.is_correct = (user_answer == self.correct_answer)
self.time_taken_s = time_taken_s
@dataclass
class SessionResult:
started_at: float
ended_at: float
mode: str
difficulty: str
num_questions: int
correct: int
wrong: int
accuracy_percent: float
average_time_s: float
questions: List[Question]
# -----------------------------
# Utilities
# -----------------------------
def clear_screen() -> None:
# Simple clear (portable-ish)
print("\n" * 80)
def read_int(prompt: str, *, min_val: int | None = None, max_val: int | None = None) -> int:
while True:
raw = input(prompt).strip()
try:
value = int(raw)
except ValueError:
print("Please enter a valid integer.")
continue
if min_val is not None and value < min_val:
print(f"Value must be >= {min_val}.")
continue
if max_val is not None and value > max_val:
print(f"Value must be <= {max_val}.")
continue
return value
def read_choice(prompt: str, choices: List[str]) -> str:
choice_map = {[Link](): c for c in choices}
while True:
raw = input(prompt).strip().lower()
if raw in choice_map:
return choice_map[raw]
print(f"Choose one of: {', '.join(choices)}")
def format_seconds(s: float) -> str:
if s < 60:
return f"{s:.2f}s"
m = int(s // 60)
r = s % 60
return f"{m}m {r:.2f}s"
# -----------------------------
# Multiplication Tables
# -----------------------------
def show_table(n: int, upto: int = 12) -> None:
print(f"\nMultiplication Table for {n} (1 to {upto})")
print("-" * 34)
for i in range(1, upto + 1):
print(f"{n:>2} × {i:>2} = {n * i:>4}")
print("-" * 34)
def show_tables_range(start: int, end: int, upto: int = 12) -> None:
for n in range(start, end + 1):
show_table(n, upto=upto)
# -----------------------------
# Quiz Generation
# -----------------------------
def difficulty_ranges(difficulty: str) -> Tuple[Tuple[int, int], Tuple[int, int]]:
"""
Returns (range for a), (range for b).
"""
if difficulty == "easy":
return (0, 10), (0, 10)
if difficulty == "medium":
return (2, 15), (2, 15)
if difficulty == "hard":
return (5, 25), (5, 25)
if difficulty == "insane":
return (10, 50), (10, 50)
# fallback
return (0, 12), (0, 12)
def make_questions(num_questions: int, difficulty: str, *, avoid_repeats: bool = True) ->
List[Question]:
(a_min, a_max), (b_min, b_max) = difficulty_ranges(difficulty)
generated: List[Question] = []
seen: set[Tuple[int, int]] = set()
while len(generated) < num_questions:
a = [Link](a_min, a_max)
b = [Link](b_min, b_max)
key = (a, b)
if avoid_repeats and key in seen:
continue
[Link](key)
[Link](Question(a=a, b=b))
return generated
# -----------------------------
# Quiz Engine
# -----------------------------
def run_quiz(
*,
difficulty: str,
num_questions: int,
timed: bool,
time_limit_s: int,
) -> SessionResult:
mode = "timed" if timed else "practice"
started = [Link]()
questions = make_questions(num_questions, difficulty, avoid_repeats=True)
correct = 0
wrong = 0
total_time = 0.0
clear_screen()
print("Multiplication Quiz")
print("===================")
print(f"Mode: {mode} | Difficulty: {difficulty} | Questions: {num_questions}")
if timed:
print(f"Time limit per question: {time_limit_s}s")
print()
for idx, q in enumerate(questions, start=1):
print(f"Q{idx}/{num_questions}: {q.a} × {q.b} = ?")
t0 = [Link]()
user_answer: Optional[int] = None
# Basic timed input (soft timing): we measure how long they took.
# Note: true "interrupt input after X seconds" needs OS-specific tricks.
raw = input("Your answer: ").strip()
t1 = [Link]()
elapsed = t1 - t0
total_time += elapsed
if raw == "":
user_answer = 0
else:
try:
user_answer = int(raw)
except ValueError:
user_answer = 0
[Link](user_answer=user_answer, time_taken_s=elapsed)
# Evaluate timing (soft enforcement)
⏱️
if timed and elapsed > time_limit_s:
print(f" Too slow! You took {format_seconds(elapsed)} (limit {time_limit_s}s).")
if q.is_correct:
# even if correct, in timed mode mark as wrong if exceeded
q.is_correct = False
wrong += 1
print(f"Correct answer was: {q.a * q.b}\n")
continue
✅
if q.is_correct:
print(" Correct!\n")
correct += 1
❌
else:
print(f" Wrong. Correct answer: {q.a * q.b}\n")
wrong += 1
ended = [Link]()
accuracy = (correct / num_questions) * 100.0 if num_questions else 0.0
avg_time = total_time / num_questions if num_questions else 0.0
return SessionResult(
started_at=started,
ended_at=ended,
mode=mode,
difficulty=difficulty,
num_questions=num_questions,
correct=correct,
wrong=wrong,
accuracy_percent=accuracy,
average_time_s=avg_time,
questions=questions,
)
# -----------------------------
# Reporting + Saving
# -----------------------------
def print_summary(result: SessionResult) -> None:
duration = result.ended_at - result.started_at
print("\nSession Summary")
print("===============")
print(f"Mode: {[Link]}")
print(f"Difficulty: {[Link]}")
print(f"Questions: {result.num_questions}")
print(f"Correct: {[Link]}")
print(f"Wrong: {[Link]}")
print(f"Accuracy: {result.accuracy_percent:.2f}%")
print(f"Average time/question: {format_seconds(result.average_time_s)}")
print(f"Total duration: {format_seconds(duration)}")
def print_mistakes(result: SessionResult) -> None:
mistakes = [q for q in [Link] if q.is_correct is False]
🧠✅
if not mistakes:
print("\nNo mistakes — clean sweep ")
return
print("\nMistakes Review")
print("==============")
for i, q in enumerate(mistakes, start=1):
print(
f"{i:>2}. {q.a} × {q.b} = {q.a*q.b} | "
f"you said: {q.user_answer} | time: {format_seconds(q.time_taken_s or 0)}"
)
def save_result(result: SessionResult, filepath: str = "multiplication_session.json") -> Path:
path = Path(filepath)
payload = {
"started_at": result.started_at,
"ended_at": result.ended_at,
"mode": [Link],
"difficulty": [Link],
"num_questions": result.num_questions,
"correct": [Link],
"wrong": [Link],
"accuracy_percent": result.accuracy_percent,
"average_time_s": result.average_time_s,
"questions": [asdict(q) for q in [Link]],
}
path.write_text([Link](payload, indent=2), encoding="utf-8")
return path
# -----------------------------
# Main Menu
# -----------------------------
def menu() -> None:
while True:
print("\nMultiplication Mastery")
print("======================")
print("1) Show a multiplication table")
print("2) Show tables in a range")
print("3) Practice quiz (untimed)")
print("4) Timed quiz")
print("5) Exit")
choice = read_choice("Select (1/2/3/4/5): ", ["1", "2", "3", "4", "5"])
if choice == "1":
n = read_int("Which number table? (0-200): ", min_val=0, max_val=200)
upto = read_int("Up to which multiplier? (1-50): ", min_val=1, max_val=50)
clear_screen()
show_table(n, upto=upto)
elif choice == "2":
start = read_int("Start table (0-200): ", min_val=0, max_val=200)
end = read_int("End table (0-200): ", min_val=0, max_val=200)
if end < start:
start, end = end, start
upto = read_int("Up to which multiplier? (1-20): ", min_val=1, max_val=20)
clear_screen()
show_tables_range(start, end, upto=upto)
elif choice == "3":
difficulty = read_choice("Difficulty (easy/medium/hard/insane): ", ["easy", "medium",
"hard", "insane"])
num_q = read_int("Number of questions (1-100): ", min_val=1, max_val=100)
result = run_quiz(difficulty=difficulty, num_questions=num_q, timed=False,
time_limit_s=0)
print_summary(result)
print_mistakes(result)
if read_choice("Save results? (y/n): ", ["y", "n"]) == "y":
path = save_result(result)
print(f"Saved to: {[Link]()}")
elif choice == "4":
difficulty = read_choice("Difficulty (easy/medium/hard/insane): ", ["easy", "medium",
"hard", "insane"])
num_q = read_int("Number of questions (1-50): ", min_val=1, max_val=50)
limit = read_int("Time limit per question in seconds (2-60): ", min_val=2, max_val=60)
result = run_quiz(difficulty=difficulty, num_questions=num_q, timed=True,
time_limit_s=limit)
print_summary(result)
print_mistakes(result)
if read_choice("Save results? (y/n): ", ["y", "n"]) == "y":
path = save_result(result)
print(f"Saved to: {[Link]()}")
👋")
else:
print("Bye
return
if __name__ == "__main__":
[Link]() # system time seed
menu()