DEV Community

Mate Technologies
Mate Technologies

Posted on

๐Ÿงฉ Building a Number Snake Puzzle Generator in Python (with PDF & JPG Export)

In this tutorial, weโ€™ll build a desktop app in Python that generates Number Snake math puzzles. The app:

Creates solvable arithmetic โ€œsnakeโ€ paths

Supports Easy (3ร—3), Medium (4ร—4), and Hard (5ร—5) grids

Shows step-by-step solutions

Exports puzzles to PDF or JPG

Can batch-generate multiple worksheets

Weโ€™ll use:

tkinter for the GUI

ttkbootstrap for modern styling

reportlab for PDF export

Pillow for JPG images

This guide is written for beginners and breaks everything into small, understandable steps.

โœ… Prerequisites

Make sure you have Python 3.9+ installed.

Then install the required packages:

pip install ttkbootstrap reportlab pillow

๐Ÿ“ Project Structure

Create a single file:

number_snake.py

Weโ€™ll place everything inside this file.

๐Ÿงฑ Step 1 โ€” Imports and Basic Setup

Start by importing the libraries 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
Enter fullscreen mode Exit fullscreen mode

What these do

tkinter โ†’ core GUI

ttkbootstrap โ†’ modern dark theme + widgets

random + operator โ†’ puzzle math

reportlab โ†’ PDF generation

Pillow โ†’ JPG images

Path โ†’ clean file handling

๐Ÿ Step 2 โ€” Create the Main App Class

Now we define our application class:

class NumberSnake:
    APP_NAME = "Number Snake Generator"
    APP_VERSION = "1.0"

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

Explanation

APP_NAME and APP_VERSION are just labels

OPERATORS maps symbols to real Python math functions

This lets us randomly choose operations later.

๐Ÿ–ฅ๏ธ Step 3 โ€” Initialize the Window

Inside init, we configure the GUI:

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()
Enter fullscreen mode Exit fullscreen mode

Whatโ€™s happening

Creates the main window

Applies a dark theme

Sets defaults for difficulty and puzzle count

Initializes empty puzzle data

Calls _build_ui() to draw the interface

๐ŸŽ›๏ธ Step 4 โ€” Build the User Interface

Now we create labels, dropdowns, and buttons:

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.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)

    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

This gives us:

Difficulty selector

Number-of-puzzles input

Buttons

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

tb.Button(ctrl, text="Generate Single Puzzle",
          bootstyle="success",
          command=self.generate_single_puzzle).pack(side="left", padx=5)

tb.Button(ctrl, text="Multiple PDFs",
          bootstyle="warning",
          command=self.generate_multiple_combined_pdf).pack(side="left", padx=5)

tb.Button(ctrl, text="JPG Export",
          bootstyle="secondary",
          command=self.generate_multiple_jpgs).pack(side="left", padx=5)
Enter fullscreen mode Exit fullscreen mode

Each button simply calls a method weโ€™ll define later.

๐Ÿง  Step 5 โ€” Generate the Snake Puzzle

This is the heart of the project.

def create_puzzle_data(self):
    diff = self.difficulty_var.get()
    self.rows, self.cols = (3,3) if diff=="Easy" else (4,4) if diff=="Medium" else (5,5)

    visited = [[False]*self.cols for _ in range(self.rows)]
    r = c = 0

    self.solution_path = [(0,0)]
    visited[0][0] = True
Enter fullscreen mode Exit fullscreen mode

Explanation

Grid size depends on difficulty

We start in the top-left

visited tracks where weโ€™ve been

Create the Snake Path

moves = [(0,1),(1,0),(0,-1),(-1,0)]

while len(self.solution_path) < self.rows * self.cols:
    random.shuffle(moves)
    for dr, dc in moves:
        nr, nc = r+dr, c+dc
        if 0<=nr<self.rows and 0<=nc<self.cols and not visited[nr][nc]:
            r, c = nr, nc
            self.solution_path.append((r,c))
            visited[r][c] = True
            break
Enter fullscreen mode Exit fullscreen mode

This randomly walks through the grid, touching every cell once.

Thatโ€™s your โ€œsnakeโ€.

โž• Step 6 โ€” Fill Numbers and Operations

numbers = [[0]*self.cols for _ in range(self.rows)]
ops = [[None]*self.cols for _ in range(self.rows)]

current = random.randint(1,9)
numbers[0][0] = current
steps = [f"Start: {current}"]
Enter fullscreen mode Exit fullscreen mode

Then for every next cell:

for r,c in self.solution_path[1:]:
    valid = False
    while not valid:
        op = random.choice(list(self.OPERATORS.keys()))
        num = random.randint(1,9)

        if op == "/" and current % num != 0:
            continue
        if op == "-" and current - num <= 0:
            continue

        next_val = self.OPERATORS[op](current, num)
        valid = True

    ops[r][c] = op
    numbers[r][c] = num
    steps.append(f"{current} {op} {num} = {next_val}")
    current = next_val
Enter fullscreen mode Exit fullscreen mode

Why the checks?

Avoid negative results

Avoid fractional division

Ensure every puzzle is solvable with integers

๐ŸŽฏ Step 7 โ€” Display the Grid and Solution

We draw labels for each cell:

def display_grid(self):
    for r in range(self.rows):
        for c in range(self.cols):
            text = str(self.grid_numbers[r][c]) \
                   if self.grid_ops[r][c] is None \
                   else f"{self.grid_ops[r][c]}{self.grid_numbers[r][c]}"
Enter fullscreen mode Exit fullscreen mode

Green cells highlight the snake path.

The solution panel prints each math step:

def show_solution(self):
    self.solution_text.delete("1.0", tk.END)
    self.solution_text.insert(tk.END, "\n".join(self.steps))
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“„ Step 8 โ€” Export to PDF

Using ReportLab:

c = canvas.Canvas("puzzle.pdf", pagesize=A4)
c.drawString(50, 800, "Number Snake Puzzle")
Enter fullscreen mode Exit fullscreen mode

We draw:

Title

Grid

Target

Step-by-step solution

Each puzzle can be saved separately or combined.

๐Ÿ–ผ๏ธ Step 9 โ€” Export to JPG

With Pillow:

img = Image.new("RGB",(900,700),(34,34,34))
draw = ImageDraw.Draw(img)
draw.text((20,20),"Number Snake Puzzle", fill="white")
Enter fullscreen mode Exit fullscreen mode

Then we:

Draw grid squares

Add numbers

Write solution steps

Save as NumberSnake_1.jpg, NumberSnake_2.jpg, etc.

Perfect for printable worksheets.

โ–ถ๏ธ Final Step โ€” Run the App

At the bottom of your file:

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

Run it:

python number_snake.py

๐ŸŽ‰ Done!

You now have a full desktop app that:

Generates arithmetic snake puzzles

Shows solutions

Exports PDFs and JPGs

Supports batch worksheet creation

Source code on GitHub:
๐Ÿ‘‰ https://github.com/rogers-cyber/NumberSnakeGenerator

Top comments (0)