In this tutorial, we’ll build MazeMath — a desktop app that generates arithmetic maze puzzles, shows step-by-step solutions, and exports worksheets as PDF or JPG.
We’ll use:
Python
Tkinter + ttkbootstrap (GUI)
ReportLab (PDF export)
Pillow (JPG export)
By the end, you’ll have a working educational puzzle generator.
👉 Final project: https://github.com/rogers-cyber/MazeMath
✅ What MazeMath Does
Generates math mazes (Easy / Medium / Hard)
Guarantees a solvable path
Shows step-by-step arithmetic
Highlights the solution visually
Exports puzzles as PDFs or JPGs
Supports multiple puzzles at once
Great for classrooms, tutoring, or learning projects.
- Installing Dependencies
First, install the required libraries:
pip install ttkbootstrap reportlab pillow
Tkinter usually comes bundled with Python.
- Importing Libraries
Start by importing everything we’ll need:
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
Why these?
tkinter → base GUI
ttkbootstrap → modern dark theme
random + operator → puzzle math
reportlab → PDF generation
Pillow → JPG images
- Creating the Main App Class
We wrap everything in a class called MazeMath.
class MazeMath:
APP_NAME = "MazeMath"
APP_VERSION = "2.1"
OPERATORS = {
"+": operator.add,
"-": operator.sub,
"*": operator.mul,
"/": operator.floordiv
}
What’s happening?
APP_NAME and APP_VERSION are just labels
OPERATORS maps math symbols to Python functions
- Initializing the Window
Inside init, we set up the main window and variables:
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("1100x680")
self.difficulty_var = tk.StringVar(value="Easy")
self.num_puzzles_var = tk.IntVar(value=1)
self.grid_numbers = []
self.grid_ops = []
self.solution_path = []
self.target_number = None
self.rows = self.cols = 0
self._build_ui()
We store:
Difficulty
Number of puzzles
Grid values
Solution path
Target result
- Building the User Interface
Now we create buttons, dropdowns, and panels:
def _build_ui(self):
tb.Label(self.root, text=self.APP_NAME,
font=("Segoe UI", 22, "bold")).pack(pady=10)
opts = tb.Labelframe(self.root, text="Options", padding=10)
opts.pack(fill="x", padx=10)
tb.Combobox(opts,
values=["Easy","Medium","Hard"],
textvariable=self.difficulty_var,
width=10).pack(side="left", padx=5)
tb.Spinbox(opts, from_=1, to=20,
textvariable=self.num_puzzles_var,
width=5).pack(side="left", padx=5)
Then we add control buttons:
tb.Button(ctrl, text="Generate",
command=self.generate_single_puzzle).pack(side="left")
tb.Button(ctrl, text="PDF",
command=self.generate_multiple_combined_pdf).pack(side="left")
tb.Button(ctrl, text="JPG",
command=self.generate_multiple_jpgs).pack(side="left")
And finally:
A grid area for the puzzle
A text box for the solution steps
- Generating a Maze Path
We use Depth-First Search to ensure every puzzle is solvable.
def generate_maze(self, rows, cols):
visited = [[False]*cols for _ in range(rows)]
path = []
def dfs(r, c):
visited[r][c] = True
path.append((r,c))
dirs = [(0,1),(1,0),(0,-1),(-1,0)]
random.shuffle(dirs)
for dr, dc in dirs:
nr, nc = r+dr, c+dc
if 0 <= nr < rows and 0 <= nc < cols and not visited[nr][nc]:
dfs(nr, nc)
dfs(0,0)
return path
This gives us a guaranteed path from start to finish.
- Creating Puzzle Data
Now we place numbers and operators along that path:
def create_puzzle_data(self):
diff = self.difficulty_var.get()
if diff == "Easy":
self.rows = self.cols = 3
elif diff == "Medium":
self.rows = self.cols = 4
else:
self.rows = self.cols = 5
Then:
path = self.generate_maze(self.rows, self.cols)
current = random.randint(1,9)
steps = [f"Start: {current}"]
For every next cell, we safely apply math:
for r,c in path[1:]:
op = random.choice(list(self.OPERATORS.keys()))
num = random.randint(1,9)
if op == "/" and current % num != 0:
continue
next_val = self.OPERATORS[op](current, num)
steps.append(f"{current} {op} {num} = {next_val}")
current = next_val
This guarantees:
No negative results
No broken division
Always solvable
- Displaying the Grid
We draw labels for each cell:
for r in range(self.rows):
for c in range(self.cols):
text = f"{op}{num}"
bg = "#4caf50" if (r,c) in self.solution_path else "#222"
tb.Label(self.grid_frame,
text=text,
background=bg).grid(row=r,column=c)
Green cells show the solution path.
- Showing Step-by-Step Solutions
def show_solution(self):
self.solution_text.delete("1.0", tk.END)
self.solution_text.insert(tk.END, "\n".join(steps))
This prints:
Start: 5
5 + 3 = 8
8 × 2 = 16
...
- Exporting to PDF
Using ReportLab:
c = canvas.Canvas("puzzle.pdf", pagesize=A4)
c.drawString(50, 800, "MazeMath Puzzle")
Then we draw:
Grid
Target
Solution steps
Each puzzle becomes a printable worksheet.
- Exporting to JPG
With Pillow:
img = Image.new("RGB", (900, 800), (34,34,34))
draw = ImageDraw.Draw(img)
We draw:
Title
Maze grid
Highlighted path
Target
Solution text
Finally:
img.save("Puzzle_1.jpg")
- Running the App
Finish with:
if __name__ == "__main__":
MazeMath().run()
Run it:
python mazemath.py
🎉 Done!
🚀 Final Thoughts
MazeMath combines:
Algorithms (DFS)
GUI design
Math logic
File export
It’s a great example of how Python can be used for real educational tools, not just scripts.
If you enjoyed this project:
⭐ Star the repo
🐛 Open issues
🤝 Submit PRs

Top comments (0)