|
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. |
|
""" |
|
|
|
if not words: |
|
return [] |
|
|
|
|
|
valid_words = list(set([word.upper() for word in words if 2 <= len(word) <= 15])) |
|
if not valid_words: |
|
return [] |
|
|
|
|
|
valid_words.sort(key=len, reverse=True) |
|
|
|
|
|
if size is None: |
|
|
|
total_chars = sum(len(word) for word in valid_words) |
|
min_size = max( |
|
math.ceil(math.sqrt(total_chars * 2)), |
|
len(valid_words[0]) |
|
) |
|
size = min_size |
|
|
|
|
|
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 [] |
|
|
|
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 |
|
""" |
|
|
|
if word_index == len(words): |
|
return True |
|
|
|
current_word = words[word_index] |
|
grid_size = len(grid) |
|
|
|
|
|
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): |
|
|
|
original_state = _place_word(grid, current_word, row, col, direction) |
|
placed_words.append((current_word, row, col, direction)) |
|
|
|
|
|
if _place_words_backtrack(grid, words, word_index + 1, placed_words): |
|
return True |
|
|
|
|
|
_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) |
|
|
|
|
|
if direction == 'horizontal': |
|
if col + word_len > grid_size: |
|
return False |
|
|
|
for i, char in enumerate(word): |
|
current_cell = grid[row][col + i] |
|
if current_cell != '.' and current_cell != char: |
|
return False |
|
else: |
|
if row + word_len > grid_size: |
|
return False |
|
|
|
for i, char in enumerate(word): |
|
current_cell = grid[row + i][col] |
|
if current_cell != '.' and current_cell != char: |
|
return False |
|
|
|
|
|
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': |
|
|
|
if col > 0 and grid[row][col - 1] != '.': |
|
return False |
|
if col + len(word) < grid_size and grid[row][col + len(word)] != '.': |
|
return False |
|
|
|
|
|
for i, char in enumerate(word): |
|
current_col = col + i |
|
if grid[row][current_col] == '.': |
|
|
|
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: |
|
|
|
if row > 0 and grid[row - 1][col] != '.': |
|
return False |
|
if row + len(word) < grid_size and grid[row + len(word)][col] != '.': |
|
return False |
|
|
|
|
|
for i, char in enumerate(word): |
|
current_row = row + i |
|
if grid[current_row][col] == '.': |
|
|
|
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. |
|
""" |
|
|
|
return True |
|
|
|
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: |
|
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 |
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
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") |
|
|
|
|
|
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") |