Initial commit: Word Search Generator application
- Interactive GUI for generating and playing word search puzzles - Drag-to-select word highlighting functionality - PDF export capability - Auto-resizing window to fit puzzle dimensions - Duplicate word prevention - 8-directional word placement (horizontal, vertical, diagonal) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
44
.gitignore
vendored
Normal file
44
.gitignore
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# IDE specific files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS specific files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Generated PDF files (optional - uncomment if you don't want to track PDFs)
|
||||||
|
# *.pdf
|
||||||
585
word_search_generator.py
Normal file
585
word_search_generator.py
Normal file
@@ -0,0 +1,585 @@
|
|||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, scrolledtext, messagebox, filedialog
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from reportlab.lib.pagesizes import letter as LETTER_SIZE
|
||||||
|
from reportlab.pdfgen import canvas as pdf_canvas
|
||||||
|
from reportlab.lib.units import inch
|
||||||
|
|
||||||
|
|
||||||
|
class WordSearchGenerator:
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.grid = [['' for _ in range(width)] for _ in range(height)]
|
||||||
|
self.placed_words = []
|
||||||
|
|
||||||
|
def place_word(self, word):
|
||||||
|
"""Try to place a word in the grid in a random direction."""
|
||||||
|
word = word.upper()
|
||||||
|
directions = [
|
||||||
|
(0, 1), # horizontal right
|
||||||
|
(1, 0), # vertical down
|
||||||
|
(1, 1), # diagonal down-right
|
||||||
|
(0, -1), # horizontal left
|
||||||
|
(-1, 0), # vertical up
|
||||||
|
(-1, -1), # diagonal up-left
|
||||||
|
(1, -1), # diagonal down-left
|
||||||
|
(-1, 1), # diagonal up-right
|
||||||
|
]
|
||||||
|
|
||||||
|
random.shuffle(directions)
|
||||||
|
|
||||||
|
for _ in range(100): # Try 100 random positions
|
||||||
|
row = random.randint(0, self.height - 1)
|
||||||
|
col = random.randint(0, self.width - 1)
|
||||||
|
|
||||||
|
for dr, dc in directions:
|
||||||
|
if self._can_place_word(word, row, col, dr, dc):
|
||||||
|
self._place_word_at(word, row, col, dr, dc)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _can_place_word(self, word, row, col, dr, dc):
|
||||||
|
"""Check if a word can be placed at the given position and direction."""
|
||||||
|
positions = []
|
||||||
|
for i, char in enumerate(word):
|
||||||
|
r = row + i * dr
|
||||||
|
c = col + i * dc
|
||||||
|
|
||||||
|
# Check bounds
|
||||||
|
if r < 0 or r >= self.height or c < 0 or c >= self.width:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if cell is empty or has the same letter
|
||||||
|
if self.grid[r][c] != '' and self.grid[r][c] != char:
|
||||||
|
return False
|
||||||
|
|
||||||
|
positions.append((r, c))
|
||||||
|
|
||||||
|
# Check if this exact word already exists at these positions
|
||||||
|
for placed_word in self.placed_words:
|
||||||
|
if set(placed_word['positions']) == set(positions):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _place_word_at(self, word, row, col, dr, dc):
|
||||||
|
"""Place a word at the given position and direction."""
|
||||||
|
positions = []
|
||||||
|
for i, char in enumerate(word):
|
||||||
|
r = row + i * dr
|
||||||
|
c = col + i * dc
|
||||||
|
self.grid[r][c] = char
|
||||||
|
positions.append((r, c))
|
||||||
|
|
||||||
|
self.placed_words.append({
|
||||||
|
'word': word,
|
||||||
|
'positions': positions,
|
||||||
|
'start': (row, col),
|
||||||
|
'direction': (dr, dc)
|
||||||
|
})
|
||||||
|
|
||||||
|
def fill_empty_cells(self):
|
||||||
|
"""Fill empty cells with random letters."""
|
||||||
|
for r in range(self.height):
|
||||||
|
for c in range(self.width):
|
||||||
|
if self.grid[r][c] == '':
|
||||||
|
self.grid[r][c] = random.choice(string.ascii_uppercase)
|
||||||
|
|
||||||
|
def get_grid_string(self):
|
||||||
|
"""Return the grid as a formatted string."""
|
||||||
|
return '\n'.join([' '.join(row) for row in self.grid])
|
||||||
|
|
||||||
|
def get_word_list_string(self):
|
||||||
|
"""Return the list of successfully placed words."""
|
||||||
|
return '\n'.join([word['word'] for word in self.placed_words])
|
||||||
|
|
||||||
|
|
||||||
|
class WordSearchApp:
|
||||||
|
def __init__(self, root):
|
||||||
|
self.root = root
|
||||||
|
self.root.title("Word Search Generator & Player")
|
||||||
|
self.root.geometry("1000x700")
|
||||||
|
|
||||||
|
# Game state
|
||||||
|
self.word_search = None
|
||||||
|
self.grid_canvas = None
|
||||||
|
self.cell_size = 30
|
||||||
|
self.cell_rects = {} # (row, col) -> canvas rectangle id
|
||||||
|
self.cell_texts = {} # (row, col) -> canvas text id
|
||||||
|
self.selected_cells = []
|
||||||
|
self.found_words = set()
|
||||||
|
self.word_labels = {}
|
||||||
|
self.is_dragging = False
|
||||||
|
|
||||||
|
# Create main frame
|
||||||
|
main_frame = ttk.Frame(root, padding="10")
|
||||||
|
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||||
|
|
||||||
|
# Configure grid weights
|
||||||
|
root.columnconfigure(0, weight=1)
|
||||||
|
root.rowconfigure(0, weight=1)
|
||||||
|
main_frame.columnconfigure(1, weight=1)
|
||||||
|
main_frame.rowconfigure(2, weight=1)
|
||||||
|
|
||||||
|
# Input section
|
||||||
|
input_frame = ttk.LabelFrame(main_frame, text="Input", padding="10")
|
||||||
|
input_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
|
||||||
|
|
||||||
|
# Dimensions
|
||||||
|
ttk.Label(input_frame, text="Width (X):").grid(row=0, column=0, sticky=tk.W)
|
||||||
|
self.width_var = tk.StringVar(value="12")
|
||||||
|
ttk.Entry(input_frame, textvariable=self.width_var, width=10).grid(row=0, column=1, sticky=tk.W, padx=(5, 20))
|
||||||
|
|
||||||
|
ttk.Label(input_frame, text="Height (Y):").grid(row=0, column=2, sticky=tk.W)
|
||||||
|
self.height_var = tk.StringVar(value="12")
|
||||||
|
ttk.Entry(input_frame, textvariable=self.height_var, width=10).grid(row=0, column=3, sticky=tk.W, padx=(5, 20))
|
||||||
|
|
||||||
|
# Words input
|
||||||
|
ttk.Label(input_frame, text="Words (one per line):").grid(row=1, column=0, columnspan=4, sticky=tk.W, pady=(10, 5))
|
||||||
|
self.words_text = scrolledtext.ScrolledText(input_frame, width=40, height=6)
|
||||||
|
self.words_text.grid(row=2, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=(0, 10))
|
||||||
|
self.words_text.insert('1.0', 'PYTHON\nGUI\nWORDSEARCH\nGAME\nPUZZLE\nFUN')
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
button_frame = ttk.Frame(input_frame)
|
||||||
|
button_frame.grid(row=3, column=0, columnspan=2, pady=10, sticky=tk.W)
|
||||||
|
ttk.Button(button_frame, text="Generate & Play", command=self.generate_word_search).pack(side=tk.LEFT, padx=5)
|
||||||
|
ttk.Button(button_frame, text="Clear Selection", command=self.clear_selection).pack(side=tk.LEFT, padx=5)
|
||||||
|
ttk.Button(button_frame, text="Export to PDF", command=self.export_to_pdf).pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
# Rules section - place to the right of buttons
|
||||||
|
rules_frame = ttk.LabelFrame(input_frame, text="How to Play", padding="5")
|
||||||
|
rules_frame.grid(row=3, column=2, columnspan=2, rowspan=1, sticky=(tk.W, tk.E, tk.N), padx=(20, 0))
|
||||||
|
rules_text = (
|
||||||
|
"• Words: horizontal, vertical, diagonal\n"
|
||||||
|
"• Can go forwards or backwards\n"
|
||||||
|
"• Click and drag to select • Green = found!"
|
||||||
|
)
|
||||||
|
ttk.Label(rules_frame, text=rules_text, justify=tk.LEFT, font=('TkDefaultFont', 9)).pack(anchor=tk.W)
|
||||||
|
|
||||||
|
# Game section (grid on left, word list on right)
|
||||||
|
game_frame = ttk.LabelFrame(main_frame, text="Play Word Search", padding="10")
|
||||||
|
game_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||||
|
game_frame.columnconfigure(0, weight=3)
|
||||||
|
game_frame.columnconfigure(1, weight=1)
|
||||||
|
game_frame.rowconfigure(0, weight=1)
|
||||||
|
|
||||||
|
# Grid frame
|
||||||
|
self.grid_frame = ttk.Frame(game_frame)
|
||||||
|
self.grid_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 10))
|
||||||
|
|
||||||
|
# Word list frame
|
||||||
|
word_list_frame = ttk.LabelFrame(game_frame, text="Words to Find", padding="10")
|
||||||
|
word_list_frame.grid(row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||||
|
word_list_frame.rowconfigure(0, weight=1)
|
||||||
|
word_list_frame.columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
# Scrollable word list
|
||||||
|
self.word_list_canvas = tk.Canvas(word_list_frame, width=150)
|
||||||
|
scrollbar = ttk.Scrollbar(word_list_frame, orient="vertical", command=self.word_list_canvas.yview)
|
||||||
|
self.word_list_inner = ttk.Frame(self.word_list_canvas)
|
||||||
|
|
||||||
|
self.word_list_canvas.configure(yscrollcommand=scrollbar.set)
|
||||||
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
self.word_list_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
self.word_list_canvas.create_window((0, 0), window=self.word_list_inner, anchor="nw")
|
||||||
|
|
||||||
|
self.word_list_inner.bind("<Configure>", lambda e: self.word_list_canvas.configure(scrollregion=self.word_list_canvas.bbox("all")))
|
||||||
|
|
||||||
|
def clear_selection(self):
|
||||||
|
"""Clear the current selection."""
|
||||||
|
if not self.grid_canvas:
|
||||||
|
return
|
||||||
|
for row, col in self.selected_cells:
|
||||||
|
if (row, col) not in self._get_all_found_positions():
|
||||||
|
rect_id = self.cell_rects.get((row, col))
|
||||||
|
if rect_id:
|
||||||
|
self.grid_canvas.itemconfig(rect_id, fill='white')
|
||||||
|
self.selected_cells = []
|
||||||
|
|
||||||
|
def _get_all_found_positions(self):
|
||||||
|
"""Get all positions that are part of found words."""
|
||||||
|
positions = set()
|
||||||
|
if self.word_search:
|
||||||
|
for word_info in self.word_search.placed_words:
|
||||||
|
if word_info['word'] in self.found_words:
|
||||||
|
positions.update(word_info['positions'])
|
||||||
|
return positions
|
||||||
|
|
||||||
|
def get_cell_from_coords(self, x, y):
|
||||||
|
"""Get cell (row, col) from canvas coordinates."""
|
||||||
|
if not self.word_search:
|
||||||
|
return None
|
||||||
|
|
||||||
|
col = x // self.cell_size
|
||||||
|
row = y // self.cell_size
|
||||||
|
|
||||||
|
height = len(self.word_search.grid)
|
||||||
|
width = len(self.word_search.grid[0]) if height > 0 else 0
|
||||||
|
|
||||||
|
if 0 <= row < height and 0 <= col < width:
|
||||||
|
return (row, col)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def on_canvas_press(self, event):
|
||||||
|
"""Handle mouse button press on canvas."""
|
||||||
|
cell = self.get_cell_from_coords(event.x, event.y)
|
||||||
|
if cell is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.is_dragging = True
|
||||||
|
self.clear_selection()
|
||||||
|
row, col = cell
|
||||||
|
self.selected_cells.append((row, col))
|
||||||
|
if (row, col) not in self._get_all_found_positions():
|
||||||
|
rect_id = self.cell_rects.get((row, col))
|
||||||
|
if rect_id:
|
||||||
|
self.grid_canvas.itemconfig(rect_id, fill='lightblue')
|
||||||
|
|
||||||
|
def on_canvas_drag(self, event):
|
||||||
|
"""Handle mouse drag on canvas."""
|
||||||
|
if not self.is_dragging:
|
||||||
|
return
|
||||||
|
|
||||||
|
cell = self.get_cell_from_coords(event.x, event.y)
|
||||||
|
if cell is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
row, col = cell
|
||||||
|
|
||||||
|
# Only add if not already selected and forms a valid line
|
||||||
|
if (row, col) not in self.selected_cells:
|
||||||
|
# Check if this continues a valid line
|
||||||
|
temp_cells = self.selected_cells + [(row, col)]
|
||||||
|
if self._is_valid_line_list(temp_cells):
|
||||||
|
self.selected_cells.append((row, col))
|
||||||
|
if (row, col) not in self._get_all_found_positions():
|
||||||
|
rect_id = self.cell_rects.get((row, col))
|
||||||
|
if rect_id:
|
||||||
|
self.grid_canvas.itemconfig(rect_id, fill='lightblue')
|
||||||
|
|
||||||
|
def on_canvas_release(self, event):
|
||||||
|
"""Handle mouse button release."""
|
||||||
|
self.is_dragging = False
|
||||||
|
self.check_for_word()
|
||||||
|
|
||||||
|
def check_for_word(self):
|
||||||
|
"""Check if the selected cells form a valid word."""
|
||||||
|
if len(self.selected_cells) < 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Sort cells to form a line
|
||||||
|
if not self._is_valid_line():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the word formed by selected cells
|
||||||
|
selected_word = ''.join([self.word_search.grid[r][c] for r, c in self.selected_cells])
|
||||||
|
|
||||||
|
# Check if it matches any unfound word (forward or backward)
|
||||||
|
for word_info in self.word_search.placed_words:
|
||||||
|
word = word_info['word']
|
||||||
|
if word in self.found_words:
|
||||||
|
continue
|
||||||
|
|
||||||
|
positions = word_info['positions']
|
||||||
|
if (set(self.selected_cells) == set(positions) or
|
||||||
|
set(self.selected_cells) == set(reversed(positions))):
|
||||||
|
# Found a word!
|
||||||
|
self.mark_word_found(word, positions)
|
||||||
|
return
|
||||||
|
|
||||||
|
def _is_valid_line(self):
|
||||||
|
"""Check if selected cells form a straight line."""
|
||||||
|
return self._is_valid_line_list(self.selected_cells)
|
||||||
|
|
||||||
|
def _is_valid_line_list(self, cells):
|
||||||
|
"""Check if a list of cells forms a straight line."""
|
||||||
|
if len(cells) < 2:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Get direction from first two cells
|
||||||
|
first = cells[0]
|
||||||
|
second = cells[1]
|
||||||
|
|
||||||
|
dr = second[0] - first[0]
|
||||||
|
dc = second[1] - first[1]
|
||||||
|
|
||||||
|
# Normalize direction
|
||||||
|
if dr != 0:
|
||||||
|
dr = dr // abs(dr)
|
||||||
|
if dc != 0:
|
||||||
|
dc = dc // abs(dc)
|
||||||
|
|
||||||
|
# Check if all cells follow the same direction
|
||||||
|
for i in range(len(cells) - 1):
|
||||||
|
curr = cells[i]
|
||||||
|
next_cell = cells[i + 1]
|
||||||
|
expected_next = (curr[0] + dr, curr[1] + dc)
|
||||||
|
if next_cell != expected_next:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def mark_word_found(self, word, positions):
|
||||||
|
"""Mark a word as found."""
|
||||||
|
self.found_words.add(word)
|
||||||
|
|
||||||
|
# Highlight the found word in green
|
||||||
|
for row, col in positions:
|
||||||
|
rect_id = self.cell_rects.get((row, col))
|
||||||
|
if rect_id:
|
||||||
|
self.grid_canvas.itemconfig(rect_id, fill='lightgreen')
|
||||||
|
|
||||||
|
# Strike through the word in the list
|
||||||
|
if word in self.word_labels:
|
||||||
|
self.word_labels[word].config(font=('TkDefaultFont', 10, 'overstrike'))
|
||||||
|
|
||||||
|
# Clear selection
|
||||||
|
self.selected_cells = []
|
||||||
|
|
||||||
|
# Check if all words are found
|
||||||
|
if len(self.found_words) == len(self.word_search.placed_words):
|
||||||
|
messagebox.showinfo("Congratulations!", "You found all the words!")
|
||||||
|
|
||||||
|
def create_grid_ui(self):
|
||||||
|
"""Create the interactive grid UI using Canvas."""
|
||||||
|
# Clear existing grid
|
||||||
|
for widget in self.grid_frame.winfo_children():
|
||||||
|
widget.destroy()
|
||||||
|
|
||||||
|
self.cell_rects = {}
|
||||||
|
self.cell_texts = {}
|
||||||
|
|
||||||
|
height = len(self.word_search.grid)
|
||||||
|
width = len(self.word_search.grid[0]) if height > 0 else 0
|
||||||
|
|
||||||
|
# Create canvas
|
||||||
|
canvas_width = width * self.cell_size
|
||||||
|
canvas_height = height * self.cell_size
|
||||||
|
self.grid_canvas = tk.Canvas(self.grid_frame, width=canvas_width, height=canvas_height,
|
||||||
|
bg='white', highlightthickness=1, highlightbackground='gray')
|
||||||
|
self.grid_canvas.pack()
|
||||||
|
|
||||||
|
# Draw grid cells
|
||||||
|
for row in range(height):
|
||||||
|
for col in range(width):
|
||||||
|
x1 = col * self.cell_size
|
||||||
|
y1 = row * self.cell_size
|
||||||
|
x2 = x1 + self.cell_size
|
||||||
|
y2 = y1 + self.cell_size
|
||||||
|
|
||||||
|
# Create rectangle
|
||||||
|
rect_id = self.grid_canvas.create_rectangle(x1, y1, x2, y2,
|
||||||
|
fill='white',
|
||||||
|
outline='gray',
|
||||||
|
width=1)
|
||||||
|
self.cell_rects[(row, col)] = rect_id
|
||||||
|
|
||||||
|
# Create text
|
||||||
|
letter = self.word_search.grid[row][col]
|
||||||
|
text_x = x1 + self.cell_size // 2
|
||||||
|
text_y = y1 + self.cell_size // 2
|
||||||
|
text_id = self.grid_canvas.create_text(text_x, text_y,
|
||||||
|
text=letter,
|
||||||
|
font=('Courier', 14, 'bold'),
|
||||||
|
fill='black')
|
||||||
|
self.cell_texts[(row, col)] = text_id
|
||||||
|
|
||||||
|
# Bind mouse events to canvas
|
||||||
|
self.grid_canvas.bind('<Button-1>', self.on_canvas_press)
|
||||||
|
self.grid_canvas.bind('<B1-Motion>', self.on_canvas_drag)
|
||||||
|
self.grid_canvas.bind('<ButtonRelease-1>', self.on_canvas_release)
|
||||||
|
|
||||||
|
def update_word_list(self):
|
||||||
|
"""Update the word list display."""
|
||||||
|
# Clear existing words
|
||||||
|
for widget in self.word_list_inner.winfo_children():
|
||||||
|
widget.destroy()
|
||||||
|
|
||||||
|
self.word_labels = {}
|
||||||
|
|
||||||
|
# Add each word
|
||||||
|
for i, word_info in enumerate(self.word_search.placed_words):
|
||||||
|
word = word_info['word']
|
||||||
|
label = tk.Label(self.word_list_inner, text=word, font=('TkDefaultFont', 10),
|
||||||
|
anchor='w', padx=10, pady=5)
|
||||||
|
label.pack(fill=tk.X)
|
||||||
|
self.word_labels[word] = label
|
||||||
|
|
||||||
|
def generate_word_search(self):
|
||||||
|
"""Generate the word search puzzle."""
|
||||||
|
try:
|
||||||
|
width = int(self.width_var.get())
|
||||||
|
height = int(self.height_var.get())
|
||||||
|
|
||||||
|
if width < 5 or width > 25 or height < 5 or height > 25:
|
||||||
|
messagebox.showerror("Error", "Dimensions must be between 5 and 25")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get words
|
||||||
|
words_input = self.words_text.get('1.0', tk.END).strip()
|
||||||
|
words = [word.strip() for word in words_input.split('\n') if word.strip()]
|
||||||
|
|
||||||
|
if not words:
|
||||||
|
messagebox.showerror("Error", "Please enter at least one word")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create word search
|
||||||
|
ws = WordSearchGenerator(width, height)
|
||||||
|
|
||||||
|
failed_words = []
|
||||||
|
for word in words:
|
||||||
|
if len(word) > max(width, height):
|
||||||
|
failed_words.append(f"{word} (too long)")
|
||||||
|
elif not ws.place_word(word):
|
||||||
|
failed_words.append(f"{word} (couldn't fit)")
|
||||||
|
|
||||||
|
ws.fill_empty_cells()
|
||||||
|
|
||||||
|
if len(ws.placed_words) == 0:
|
||||||
|
messagebox.showerror("Error", "No words could be placed. Try increasing grid size.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Store the word search
|
||||||
|
self.word_search = ws
|
||||||
|
self.found_words = set()
|
||||||
|
self.selected_cells = []
|
||||||
|
|
||||||
|
# Create the interactive UI
|
||||||
|
self.create_grid_ui()
|
||||||
|
self.update_word_list()
|
||||||
|
|
||||||
|
# Resize window to fit puzzle
|
||||||
|
self.resize_window_for_puzzle(width, height)
|
||||||
|
|
||||||
|
if failed_words:
|
||||||
|
messagebox.showwarning("Warning",
|
||||||
|
f"{len(failed_words)} word(s) couldn't be placed. Try increasing grid size or removing some words.")
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
messagebox.showerror("Error", "Please enter valid numbers for dimensions")
|
||||||
|
|
||||||
|
def resize_window_for_puzzle(self, grid_width, grid_height):
|
||||||
|
"""Resize the window to fit the puzzle comfortably."""
|
||||||
|
# Calculate required size for puzzle grid
|
||||||
|
puzzle_width = grid_width * self.cell_size
|
||||||
|
puzzle_height = grid_height * self.cell_size
|
||||||
|
|
||||||
|
# Add generous padding for UI elements:
|
||||||
|
# - Input section height: ~280px
|
||||||
|
# - Right panel (word list) width: ~280px
|
||||||
|
# - Frame padding, borders, and extra space: ~120px width, ~120px height
|
||||||
|
total_width = puzzle_width + 280 + 120
|
||||||
|
total_height = puzzle_height + 280 + 120
|
||||||
|
|
||||||
|
# Set minimum based on puzzle size to ensure it fits
|
||||||
|
min_width = max(900, total_width)
|
||||||
|
min_height = max(700, total_height)
|
||||||
|
max_width = 1800
|
||||||
|
max_height = 1200
|
||||||
|
|
||||||
|
# Clamp to maximum bounds only
|
||||||
|
final_width = min(max_width, min_width)
|
||||||
|
final_height = min(max_height, min_height)
|
||||||
|
|
||||||
|
# Resize the window
|
||||||
|
self.root.geometry(f"{final_width}x{final_height}")
|
||||||
|
|
||||||
|
# Update the window to apply changes
|
||||||
|
self.root.update_idletasks()
|
||||||
|
|
||||||
|
def export_to_pdf(self):
|
||||||
|
"""Export the current word search puzzle to a PDF file."""
|
||||||
|
if not self.word_search:
|
||||||
|
messagebox.showerror("Error", "Please generate a word search puzzle first")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ask user for save location
|
||||||
|
file_path = filedialog.asksaveasfilename(
|
||||||
|
defaultextension=".pdf",
|
||||||
|
filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")],
|
||||||
|
title="Save Word Search as PDF"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not file_path:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create PDF
|
||||||
|
pdf = pdf_canvas.Canvas(file_path, pagesize=LETTER_SIZE)
|
||||||
|
page_width, page_height = LETTER_SIZE
|
||||||
|
|
||||||
|
# Title
|
||||||
|
pdf.setFont("Helvetica-Bold", 20)
|
||||||
|
pdf.drawCentredString(page_width / 2, page_height - 0.75 * inch, "Word Search Puzzle")
|
||||||
|
|
||||||
|
# Grid setup
|
||||||
|
height = len(self.word_search.grid)
|
||||||
|
width = len(self.word_search.grid[0]) if height > 0 else 0
|
||||||
|
|
||||||
|
# Calculate cell size to fit on page
|
||||||
|
max_grid_width = 6.5 * inch
|
||||||
|
max_grid_height = 6.5 * inch
|
||||||
|
cell_size = min(max_grid_width / width, max_grid_height / height, 0.4 * inch)
|
||||||
|
|
||||||
|
# Center the grid
|
||||||
|
grid_width = width * cell_size
|
||||||
|
grid_height = height * cell_size
|
||||||
|
start_x = (page_width - grid_width) / 2
|
||||||
|
start_y = page_height - 2 * inch
|
||||||
|
|
||||||
|
# Draw grid
|
||||||
|
pdf.setFont("Courier-Bold", int(cell_size * 0.6))
|
||||||
|
for row in range(height):
|
||||||
|
for col in range(width):
|
||||||
|
x = start_x + col * cell_size
|
||||||
|
y = start_y - row * cell_size
|
||||||
|
|
||||||
|
# Draw letter centered in cell (no border)
|
||||||
|
letter = self.word_search.grid[row][col]
|
||||||
|
text_x = x + cell_size / 2
|
||||||
|
text_y = y - cell_size / 2 - cell_size * 0.15
|
||||||
|
pdf.drawCentredString(text_x, text_y, letter)
|
||||||
|
|
||||||
|
# Word list
|
||||||
|
word_list_y = start_y - grid_height - 0.5 * inch
|
||||||
|
pdf.setFont("Helvetica-Bold", 12)
|
||||||
|
pdf.drawString(1 * inch, word_list_y, "Words to Find:")
|
||||||
|
|
||||||
|
pdf.setFont("Helvetica", 10)
|
||||||
|
words = [word_info['word'] for word_info in self.word_search.placed_words]
|
||||||
|
|
||||||
|
# Arrange words in columns
|
||||||
|
words_per_column = 10
|
||||||
|
col_width = 1.5 * inch
|
||||||
|
current_y = word_list_y - 0.25 * inch
|
||||||
|
|
||||||
|
for i, word in enumerate(words):
|
||||||
|
col_num = i // words_per_column
|
||||||
|
row_num = i % words_per_column
|
||||||
|
|
||||||
|
x = 1 * inch + col_num * col_width
|
||||||
|
y = current_y - row_num * 0.2 * inch
|
||||||
|
|
||||||
|
pdf.drawString(x, y, f"• {word}")
|
||||||
|
|
||||||
|
# Save PDF
|
||||||
|
pdf.save()
|
||||||
|
|
||||||
|
messagebox.showinfo("Success", f"Word search exported to:\n{file_path}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("Error", f"Failed to export PDF:\n{str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
root = tk.Tk()
|
||||||
|
app = WordSearchApp(root)
|
||||||
|
root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user