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("", 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('', self.on_canvas_press) self.grid_canvas.bind('', self.on_canvas_drag) self.grid_canvas.bind('', 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()