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:
2025-12-06 08:14:30 -07:00
commit ec142fd638
2 changed files with 629 additions and 0 deletions

44
.gitignore vendored Normal file
View 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
View 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()