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)