abc123 / hack /crossword.py
vimalk78's picture
feat(crossword): generated crosswords with clues
486eff6
import math
from typing import List, Optional, Tuple
def place_words_in_crossword(words: List[str], size: Optional[int] = None) -> List[List[str]]:
"""
Places words in a square grid to form a crossword puzzle using backtracking.
Args:
words: List of uppercase words to place (2-15 chars each)
size: Optional grid size (n x n). If None, automatically calculated
Returns:
2D list representing the grid with letters or '.' for empty cells.
Returns empty list if placement is impossible.
"""
# Handle edge cases
if not words:
return []
# Remove duplicates and filter valid words
valid_words = list(set([word.upper() for word in words if 2 <= len(word) <= 15]))
if not valid_words:
return []
# Sort words by length descending for better placement strategy
valid_words.sort(key=len, reverse=True)
# Calculate grid size if not provided
if size is None:
# Estimate based on total character count and longest word
total_chars = sum(len(word) for word in valid_words)
min_size = max(
math.ceil(math.sqrt(total_chars * 2)), # Character density estimate
len(valid_words[0]) # Must fit longest word
)
size = min_size
# Try progressively larger grids if placement fails
max_attempts = 3
for attempt in range(max_attempts):
current_size = size + attempt
grid = _create_empty_grid(current_size)
if _place_words_backtrack(grid, valid_words, 0, []):
return grid
return [] # Failed to place all words
def _create_empty_grid(size: int) -> List[List[str]]:
"""Creates an empty grid filled with dots."""
return [['.' for _ in range(size)] for _ in range(size)]
def _place_words_backtrack(grid: List[List[str]], words: List[str],
word_index: int, placed_words: List[Tuple]) -> bool:
"""
Recursive backtracking function to place words in the grid.
Args:
grid: Current state of the grid
words: List of words to place
word_index: Index of current word being placed
placed_words: List of (word, row, col, direction) tuples for placed words
Returns:
True if all words can be placed, False otherwise
"""
# Base case: all words placed successfully
if word_index == len(words):
return True
current_word = words[word_index]
grid_size = len(grid)
# Try all possible positions and orientations
for row in range(grid_size):
for col in range(grid_size):
for direction in ['horizontal', 'vertical']:
if _can_place_word(grid, current_word, row, col, direction):
# Place the word
original_state = _place_word(grid, current_word, row, col, direction)
placed_words.append((current_word, row, col, direction))
# Recursively try to place remaining words
if _place_words_backtrack(grid, words, word_index + 1, placed_words):
return True
# Backtrack: remove the word and restore grid state
_remove_word(grid, current_word, row, col, direction, original_state)
placed_words.pop()
return False
def _can_place_word(grid: List[List[str]], word: str, row: int, col: int, direction: str) -> bool:
"""
Checks if a word can be placed at the given position and direction.
Args:
grid: Current grid state
word: Word to place
row, col: Starting position
direction: 'horizontal' or 'vertical'
Returns:
True if word can be placed, False otherwise
"""
grid_size = len(grid)
word_len = len(word)
# Check if word fits in grid bounds
if direction == 'horizontal':
if col + word_len > grid_size:
return False
# Check each position
for i, char in enumerate(word):
current_cell = grid[row][col + i]
if current_cell != '.' and current_cell != char:
return False
else: # vertical
if row + word_len > grid_size:
return False
# Check each position
for i, char in enumerate(word):
current_cell = grid[row + i][col]
if current_cell != '.' and current_cell != char:
return False
# Additional validation: ensure word doesn't create invalid adjacent letters
return _validate_word_placement(grid, word, row, col, direction)
def _validate_word_placement(grid: List[List[str]], word: str, row: int, col: int, direction: str) -> bool:
"""
Validates that placing a word doesn't create invalid letter adjacencies.
"""
grid_size = len(grid)
if direction == 'horizontal':
# Check cells before and after the word
if col > 0 and grid[row][col - 1] != '.':
return False
if col + len(word) < grid_size and grid[row][col + len(word)] != '.':
return False
# Check perpendicular adjacencies
for i, char in enumerate(word):
current_col = col + i
if grid[row][current_col] == '.': # Only check empty cells
# Check above and below
if row > 0 and grid[row - 1][current_col] != '.' and not _has_crossing_word(grid, row - 1, current_col, 'vertical'):
return False
if row < grid_size - 1 and grid[row + 1][current_col] != '.' and not _has_crossing_word(grid, row + 1, current_col, 'vertical'):
return False
else: # vertical
# Check cells before and after the word
if row > 0 and grid[row - 1][col] != '.':
return False
if row + len(word) < grid_size and grid[row + len(word)][col] != '.':
return False
# Check perpendicular adjacencies
for i, char in enumerate(word):
current_row = row + i
if grid[current_row][col] == '.': # Only check empty cells
# Check left and right
if col > 0 and grid[current_row][col - 1] != '.' and not _has_crossing_word(grid, current_row, col - 1, 'horizontal'):
return False
if col < grid_size - 1 and grid[current_row][col + 1] != '.' and not _has_crossing_word(grid, current_row, col + 1, 'horizontal'):
return False
return True
def _has_crossing_word(grid: List[List[str]], row: int, col: int, direction: str) -> bool:
"""
Checks if there's a word crossing at the given position in the specified direction.
"""
# This is a simplified check - in a full implementation, you'd track placed words
return True # For now, allow crossings
def _place_word(grid: List[List[str]], word: str, row: int, col: int, direction: str) -> List[Tuple]:
"""
Places a word in the grid and returns the original state for backtracking.
Returns:
List of (row, col, original_char) tuples for restoration
"""
original_state = []
if direction == 'horizontal':
for i, char in enumerate(word):
original_state.append((row, col + i, grid[row][col + i]))
grid[row][col + i] = char
else: # vertical
for i, char in enumerate(word):
original_state.append((row + i, col, grid[row + i][col]))
grid[row + i][col] = char
return original_state
def _remove_word(grid: List[List[str]], word: str, row: int, col: int,
direction: str, original_state: List[Tuple]) -> None:
"""
Removes a word from the grid by restoring the original state.
"""
for r, c, original_char in original_state:
grid[r][c] = original_char
# Example usage
if __name__ == "__main__":
# Test with simple words
words = ["CAT", "DOG", "ACT"]
grid = place_words_in_crossword(words)
if grid:
print("Generated crossword grid:")
print('\n'.join(' '.join(row) for row in grid))
else:
print("Could not generate crossword grid")
print("\n" + "="*30 + "\n")
# Test with longer words
words2 = ["HELLO", "WORLD", "PYTHON"]
grid2 = place_words_in_crossword(words2)
if grid2:
print("Generated crossword grid for longer words:")
print('\n'.join(' '.join(row) for row in grid2))
else:
print("Could not generate crossword grid for longer words")