DEV Community

Mate Technologies
Mate Technologies

Posted on

🧩 Building NumMaze: A Python GUI Arithmetic Puzzle Generator (Step-by-Step)

In this tutorial, we’ll build NumMaze β€” a desktop Python app that generates arithmetic puzzles with automatic solutions and exports them as PDFs or JPG images.

It’s designed to be beginner-friendly and useful for:

Learning Python GUI basics

Practicing recursion and logic

Creating printable math worksheets

We’ll use:

tkinter – GUI framework

ttkbootstrap – modern themes

reportlab – PDF export

Pillow – JPG image export

By the end, you’ll have a complete puzzle generator.

βœ… Step 1 β€” Install Dependencies

First, install the required libraries:

pip install ttkbootstrap reportlab pillow

tkinter comes bundled with most Python installs.

βœ… Step 2 β€” Import Modules

Create a file called nummaze.py and start with imports:

import tkinter as tk
from tkinter import messagebox, filedialog
import random
import operator
import ttkbootstrap as tb
from ttkbootstrap.constants import *

from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4

from PIL import Image, ImageDraw, ImageFont
from pathlib import Path
Enter fullscreen mode Exit fullscreen mode

What these do

tkinter β†’ GUI

random + operator β†’ puzzle logic

ttkbootstrap β†’ modern styling

reportlab β†’ PDF generation

Pillow β†’ JPG images

βœ… Step 3 β€” Create the App Class

We wrap everything inside a class:

class NumMaze:
    APP_NAME = "NumMaze"
    APP_VERSION = "2.2.0"
Enter fullscreen mode Exit fullscreen mode

Define math operators:

    OPERATORS = {
        "+": operator.add,
        "-": operator.sub,
        "*": operator.mul,
        "/": operator.floordiv
    }

Enter fullscreen mode Exit fullscreen mode

This lets us dynamically apply math later.

βœ… Step 4 β€” Initialize the Window

Inside init:

    def __init__(self):
        self.root = tk.Tk()
        tb.Style(theme="darkly")

        self.root.title(f"{self.APP_NAME} v{self.APP_VERSION}")
        self.root.geometry("1250x700")
Enter fullscreen mode Exit fullscreen mode

We also define app state:

        self.difficulty_var = tk.StringVar(value="Easy")
        self.num_puzzles_var = tk.IntVar(value=1)

        self.grid_numbers = []
        self.target_number = None
        self.solution_steps = []

        self.rows = self.cols = 0
Enter fullscreen mode Exit fullscreen mode

Finally:

   self._build_ui()
Enter fullscreen mode Exit fullscreen mode

βœ… Step 5 β€” Build the User Interface

Create _build_ui():

    def _build_ui(self):
        tb.Label(self.root, text=self.APP_NAME,
                 font=("Segoe UI", 22, "bold")).pack(pady=10)
Enter fullscreen mode Exit fullscreen mode

Subtitle:

        tb.Label(
            self.root,
            text="Auto-Generated Arithmetic Puzzle",
            font=("Segoe UI", 10, "italic")
        ).pack()
Enter fullscreen mode Exit fullscreen mode

Options Panel

        opts = tb.Labelframe(self.root, text="Options", padding=10)
        opts.pack(fill="x", padx=10)

        tb.Label(opts, text="Difficulty:").pack(side="left")
        tb.Combobox(
            opts,
            values=["Easy","Medium","Hard"],
            textvariable=self.difficulty_var,
            width=10
        ).pack(side="left", padx=5)
Enter fullscreen mode Exit fullscreen mode

Puzzle count:

        tb.Label(opts, text="Number of Puzzles:").pack(side="left", padx=10)
        tb.Spinbox(opts, from_=1, to=20,
                   textvariable=self.num_puzzles_var,
                   width=5).pack(side="left")
Enter fullscreen mode Exit fullscreen mode

βœ… Step 6 β€” Control Buttons

        ctrl = tb.Frame(self.root)
        ctrl.pack(fill="x", padx=10, pady=10)

        tb.Button(ctrl, text="Generate Puzzle",
                  bootstyle="success",
                  command=self.generate_single_puzzle).pack(side="left")
Enter fullscreen mode Exit fullscreen mode

You can add export buttons the same way:

        tb.Button(ctrl, text="Combined PDF",
                  bootstyle="warning",
                  command=self.generate_multiple_combined_pdf).pack(side="left", padx=5)
Enter fullscreen mode Exit fullscreen mode

Each button simply calls a method.

βœ… Step 7 β€” Grid + Solution Panels

Puzzle grid:

        self.grid_frame = tb.Labelframe(self.root, text="Puzzle Grid", padding=10)
        self.grid_frame.pack(fill="x", padx=10)
Enter fullscreen mode Exit fullscreen mode

Solution area:

        sol = tb.Labelframe(self.root, text="Solution", padding=10)
        sol.pack(fill="both", expand=True, padx=10)

        self.solution_text = tk.Text(sol, height=10, font=("Consolas", 12))
        self.solution_text.pack(fill="both", expand=True)
Enter fullscreen mode Exit fullscreen mode

βœ… Step 8 β€” Generate Puzzle Data

Difficulty controls how many numbers:

    def create_puzzle_data(self):
        d = self.difficulty_var.get()

        if d == "Easy":
            n, rows, cols = 4, 2, 2
        elif d == "Medium":
            n, rows, cols = 6, 2, 3
        else:
            n, rows, cols = 9, 3, 3
Enter fullscreen mode Exit fullscreen mode

Generate random numbers:

        numbers = [random.randint(1,15) for _ in range(n)]
Enter fullscreen mode Exit fullscreen mode

Solve them:

        expr, target, steps = self.recursive_solution(numbers)
        return numbers, target, steps, rows, cols
Enter fullscreen mode Exit fullscreen mode

βœ… Step 9 β€” Recursive Solver

This repeatedly tries random combinations:

    def recursive_solution(self, numbers):
        for _ in range(5000):
            nums = numbers[:]
            random.shuffle(nums)
            expr, val, steps = self.build_expr(nums)
            if val is not None and val > 0:
                return expr, val, steps

        return str(numbers[0]), numbers[0], []
Enter fullscreen mode Exit fullscreen mode

Build Expressions

    def build_expr(self, nums):
        if len(nums) == 1:
            return str(nums[0]), nums[0], []
Enter fullscreen mode Exit fullscreen mode

Split list and recurse:

        for i in range(1, len(nums)):
            left = nums[:i]
            right = nums[i:]
Enter fullscreen mode Exit fullscreen mode

Apply operators:

            for op in self.OPERATORS:
                try:
                    val = self.OPERATORS[op](left_val, right_val)
                    steps = left_steps + right_steps + [
                        f"{left_val} {op} {right_val} = {val}"
                    ]
                    return expr, val, steps
                except:
                    continue
Enter fullscreen mode Exit fullscreen mode

This is the core logic engine.

βœ… Step 10 β€” Display Grid

    def display_grid(self):
        for w in self.grid_frame.winfo_children():
            w.destroy()
Enter fullscreen mode Exit fullscreen mode

Create labels for each number:

        for r in range(self.rows):
            for c in range(self.cols):
                tb.Label(self.grid_frame,
                         text=str(self.grid_numbers[idx]),
                         font=("Segoe UI",20,"bold")).grid(row=r,column=c)
Enter fullscreen mode Exit fullscreen mode

Target:

        tb.Label(self.grid_frame,
                 text=f"Target: {self.target_number}",
                 font=("Segoe UI",16,"bold")).grid(row=self.rows,columnspan=self.cols)
Enter fullscreen mode Exit fullscreen mode

βœ… Step 11 β€” Show Solution

    def show_solution(self):
        self.solution_text.delete("1.0", tk.END)
        self.solution_text.insert(tk.END, "\n".join(self.solution_steps))

Enter fullscreen mode Exit fullscreen mode

βœ… Step 12 β€” Export PDFs and JPGs

Using:

reportlab β†’ PDFs

Pillow β†’ images

Each export function:

Generates puzzles

Draws grids

Writes solution text

Saves files

(See full repo for complete export implementations.)

βœ… Step 13 β€” Run the App

At the bottom:

if __name__ == "__main__":
    NumMaze().run()
Enter fullscreen mode Exit fullscreen mode

πŸŽ‰ Done!

You now have:

A themed Python GUI

Recursive arithmetic solver

Puzzle grids

Step-by-step solutions

PDF + JPG exports

πŸ”— Source Code

GitHub:
https://github.com/rogers-cyber/NumMaze

Top comments (0)