Closure style
import functools
import itertools
import operator
import random
import tkinter as tk
from typing import Callable, Iterator, Sequence
from mypy.build import NamedTuple
class Operation(NamedTuple):
name: str
symbol: str
fun: Callable[[int, int], int]
MODES = (
Operation(name='Addition', symbol='+', fun=operator.add),
Operation(name='Subtraction', symbol='-', fun=operator.sub),
Operation(name='Multiplication', symbol='×', fun=operator.mul),
Operation(name='Division', symbol='÷', fun=operator.truediv),
)
class Question(NamedTuple):
operation: Operation
a: int
b: int
@classmethod
def random(cls, operation: Operation, rand: random.Random) -> 'Question':
return cls(
operation=operation,
a=rand.randint(1, 10),
b=rand.randint(1, 10),
)
def __str__(self) -> str:
return f'{self.a} {self.operation.symbol} {self.b}'
@property
def answer(self) -> int:
return self.operation.fun(self.a, self.b)
class Answer(NamedTuple):
question: Question
answer: int
@property
def is_correct(self) -> bool:
return self.answer == self.question.answer
def __str__(self) -> str:
return (
f'{self.question} = {self.question.answer}'
f' | Your answer was: {self.answer}'
)
def create_frame(root: tk.Tk) -> tk.Frame:
frame = tk.Frame(root)
frame.pack(expand=True)
root.after(ms=1, func=root.focus_force)
return frame
def set_geometry(
root: tk.Tk, width: int, height: int,
) -> None:
left = (root.winfo_screenwidth() - width)//2
top = (root.winfo_screenheight() - height)//2
root.geometry(f'{width}x{height}+{left}+{top}')
def mode_selection(
width: int = 300, height: int = 300,
) -> Operation | None:
def select(selected_option: Operation) -> None:
nonlocal option
option = selected_option
root.destroy()
root = tk.Tk()
root.title('Select Mode')
set_geometry(root=root, width=width, height=height)
frame = create_frame(root)
for mode in MODES:
button = tk.Button(
frame, text=mode.name,
command=functools.partial(select, mode),
)
button.pack(pady=5, fill=tk.X)
option: Operation | None = None
root.mainloop()
return option
def ask_questions(
questions: Sequence[Question],
mode: Operation,
width: int = 300,
height: int = 200,
) -> list[int]:
responses: list[int] = []
question_iter = enumerate(questions, start=1)
def advance() -> None:
try:
question_index, question = next(question_iter)
except StopIteration:
root.destroy()
return
question_label.config(text=str(question))
question_counter.config(
text=f'Question {question_index} out of {len(questions)}',
)
answer_var.set(value=0)
entry.focus_set()
entry.select_range(0, 'end')
def get_response(*args) -> None:
responses.append(answer_var.get())
advance()
root = tk.Tk()
root.title(mode.name)
root.bind('<Return>', get_response)
set_geometry(root=root, width=width, height=height)
frame = create_frame(root)
question_counter = tk.Label(frame, font=('Arial', 16))
question_counter.pack(pady=5)
question_label = tk.Label(frame, font=('Arial', 20))
question_label.pack(pady=5)
answer_var = tk.IntVar(frame, name='answer')
entry = tk.Entry(frame, textvariable=answer_var)
entry.pack(pady=5)
tk.Button(frame, text='Enter', command=get_response).pack(pady=5)
advance()
root.mainloop()
return responses
def show_results(
questions: Sequence[Question],
responses: Sequence[int],
width: int = 300,
height: int = 250,
questions_per_page: int = 5,
) -> None:
answers = [
Answer(question=question, answer=answer)
for question, answer in zip(questions, responses)
]
n_correct = sum(1 for answer in answers if answer.is_correct)
all_correct = n_correct == len(questions)
paged_answers = itertools.batched(
(answer for answer in answers if not answer.is_correct),
n=questions_per_page,
)
def next_page(*args) -> None:
try:
batch = next(paged_answers)
except StopIteration:
root.destroy()
return
for label, answer in zip(incorrect_labels, batch):
label.config(text=str(answer))
for label in incorrect_labels[len(batch):]:
label.config(text='')
root = tk.Tk()
root.title(f'Score: {n_correct}/{len(questions)}')
root.bind('<Return>', next_page)
set_geometry(root=root, width=width, height=height)
frame = create_frame(root)
results_title = tk.Label(
frame,
text=f'You missed {"no" if all_correct else "the following"} questions',
)
results_title.pack(pady=5)
incorrect_labels = [
tk.Label(frame)
for _ in range(questions_per_page)
]
for label in incorrect_labels:
label.pack(pady=5)
tk.Button(frame, text='Continue', command=next_page).pack(pady=5)
if not all_correct:
next_page()
root.mainloop()
def create_questions(
operation: Operation,
num_of_questions: int = 15,
rand: random.Random | None = None,
) -> Iterator[Question]:
if rand is None:
rand = random.Random()
for _ in range(num_of_questions):
yield Question.random(
operation=operation, rand=rand,
)
def run_main() -> None:
session_mode = mode_selection()
if session_mode is None:
return
questions = tuple(create_questions(session_mode))
responses = ask_questions(questions, session_mode)
show_results(questions=questions, responses=responses)
if __name__ == '__main__':
run_main()
OOP style
import functools
import itertools
import operator
import random
import tkinter as tk
from typing import Callable, Iterator, Sequence
from mypy.build import NamedTuple
class Operation(NamedTuple):
name: str
symbol: str
fun: Callable[[int, int], int]
MODES = (
Operation(name='Addition', symbol='+', fun=operator.add),
Operation(name='Subtraction', symbol='-', fun=operator.sub),
Operation(name='Multiplication', symbol='×', fun=operator.mul),
Operation(name='Division', symbol='÷', fun=operator.truediv),
)
class Question(NamedTuple):
operation: Operation
a: int
b: int
@classmethod
def random(cls, operation: Operation, rand: random.Random) -> 'Question':
return cls(
operation=operation,
a=rand.randint(1, 10),
b=rand.randint(1, 10),
)
def __str__(self) -> str:
return f'{self.a} {self.operation.symbol} {self.b}'
@property
def answer(self) -> int:
return self.operation.fun(self.a, self.b)
class Answer(NamedTuple):
question: Question
answer: int
@property
def is_correct(self) -> bool:
return self.answer == self.question.answer
def __str__(self) -> str:
return (
f'{self.question} = {self.question.answer}'
f' | Your answer was: {self.answer}'
)
def create_frame(root: tk.Tk) -> tk.Frame:
frame = tk.Frame(root)
frame.pack(expand=True)
root.after(ms=1, func=root.focus_force)
return frame
def set_geometry(
root: tk.Tk, width: int, height: int,
) -> None:
left = (root.winfo_screenwidth() - width)//2
top = (root.winfo_screenheight() - height)//2
root.geometry(f'{width}x{height}+{left}+{top}')
class ModeSelection:
def __init__(
self, width: int = 300, height: int = 300,
) -> None:
self.root = tk.Tk()
self.root.title('Select Mode')
self.mainloop = self.root.mainloop
set_geometry(root=self.root, width=width, height=height)
frame = create_frame(self.root)
for mode in MODES:
button = tk.Button(
frame, text=mode.name,
command=functools.partial(self.select, mode),
)
button.pack(pady=5, fill=tk.X)
self.mode: Operation | None = None
def select(self, selected_option: Operation) -> None:
self.mode = selected_option
self.root.destroy()
class QuestionPrompt:
def __init__(
self,
questions: Sequence[Question],
width: int = 300, height: int = 200,
) -> None:
self.root = tk.Tk()
self.root.title(questions[0].operation.name)
self.root.bind('<Return>', self.get_response)
self.mainloop = self.root.mainloop
set_geometry(root=self.root, width=width, height=height)
frame = create_frame(self.root)
self.counter_var = tk.StringVar(name='counter')
question_counter = tk.Label(frame, textvariable=self.counter_var, font=('Arial', 16))
question_counter.pack(pady=5)
self.question_var = tk.StringVar(name='question')
question_label = tk.Label(frame, textvariable=self.question_var, font=('Arial', 20))
question_label.pack(pady=5)
self.answer_var = tk.IntVar(frame, name='answer')
self.entry = tk.Entry(frame, textvariable=self.answer_var)
self.entry.pack(pady=5)
tk.Button(frame, text='Enter', command=self.get_response).pack(pady=5)
self.questions = questions
self.responses: list[int] = []
self.question_iter = enumerate(questions, start=1)
self.advance()
def advance(self) -> None:
try:
question_index, question = next(self.question_iter)
except StopIteration:
self.root.destroy()
return
self.question_var.set(str(question))
self.counter_var.set(
f'Question {question_index} out of {len(self.questions)}',
)
self.answer_var.set(value=0)
self.entry.focus_set()
self.entry.select_range(0, 'end')
def get_response(self, *args) -> None:
self.responses.append(self.answer_var.get())
self.advance()
class Results:
def __init__(
self,
questions: Sequence[Question],
responses: Sequence[int],
width: int = 300, height: int = 250,
questions_per_page: int = 5,
) -> None:
answers = [
Answer(question=question, answer=answer)
for question, answer in zip(questions, responses)
]
n_correct = sum(1 for answer in answers if answer.is_correct)
all_correct = n_correct == len(questions)
self.paged_answers = itertools.batched(
(answer for answer in answers if not answer.is_correct),
n=questions_per_page,
)
self.root = tk.Tk()
self.root.title(f'Score: {n_correct}/{len(questions)}')
self.root.bind('<Return>', self.next_page)
self.mainloop = self.root.mainloop
set_geometry(root=self.root, width=width, height=height)
frame = create_frame(self.root)
results_title = tk.Label(
frame,
text=f'You missed {"no" if all_correct else "the following"} questions',
)
results_title.pack(pady=5)
self.incorrect_vars = [
tk.StringVar(name=f'incorrect_{i}')
for i in range(questions_per_page)
]
for var in self.incorrect_vars:
tk.Label(frame, textvariable=var).pack(pady=5)
tk.Button(frame, text='Continue', command=self.next_page).pack(pady=5)
if not all_correct:
self.next_page()
def next_page(self, *args) -> None:
try:
batch = next(self.paged_answers)
except StopIteration:
self.root.destroy()
return
for label, answer in zip(self.incorrect_vars, batch):
label.set(str(answer))
for label in self.incorrect_vars[len(batch):]:
label.set('')
def create_questions(
operation: Operation,
num_of_questions: int = 15,
rand: random.Random | None = None,
) -> Iterator[Question]:
if rand is None:
rand = random.Random()
for _ in range(num_of_questions):
yield Question.random(
operation=operation, rand=rand,
)
def run_main() -> None:
selection = ModeSelection()
selection.mainloop()
if selection.mode is None:
return
questions = tuple(create_questions(selection.mode))
prompt = QuestionPrompt(questions=questions)
prompt.mainloop()
results = Results(questions=questions, responses=prompt.responses)
results.mainloop()
if __name__ == '__main__':
run_main()