Add concise docstrings to all Python modules, classes, and public functions. Configure Sphinx with napoleon extension for Google-style docstring parsing and autodoc pages for each module. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
137 lines
4.0 KiB
Python
137 lines
4.0 KiB
Python
"""Board generation for word search puzzles."""
|
|
|
|
import random
|
|
import string
|
|
|
|
from models.category import CategoryWord
|
|
from models.game import Game, Word
|
|
|
|
DIRECTIONS = {
|
|
"E": (0, 1),
|
|
"W": (0, -1),
|
|
"S": (1, 0),
|
|
"N": (-1, 0),
|
|
"SE": (1, 1),
|
|
"SW": (1, -1),
|
|
"NE": (-1, 1),
|
|
"NW": (-1, -1),
|
|
}
|
|
|
|
MAX_PLACEMENT_ATTEMPTS = 100
|
|
|
|
|
|
class BoardFactory:
|
|
"""Generates word search boards from category word lists.
|
|
|
|
Args:
|
|
session: SQLAlchemy session for querying category words.
|
|
"""
|
|
|
|
def __init__(self, session):
|
|
self.session = session
|
|
|
|
def create(self, category_id, board_size=10, word_count=8):
|
|
"""Generate a new game board.
|
|
|
|
Selects random words from the category, places them on the grid
|
|
in random directions, and fills remaining cells with random letters.
|
|
|
|
Args:
|
|
category_id: ID of the category to pull words from.
|
|
board_size: Grid dimension (N x N).
|
|
word_count: Maximum number of words to place.
|
|
|
|
Returns:
|
|
Tuple of (Game, list[Word]) with objects not yet committed.
|
|
"""
|
|
candidates = (
|
|
self.session.query(CategoryWord)
|
|
.filter(CategoryWord.category_id == category_id)
|
|
.all()
|
|
)
|
|
# Filter words that fit in the grid
|
|
candidates = [cw for cw in candidates if len(cw.word) <= board_size]
|
|
random.shuffle(candidates)
|
|
selected = candidates[:word_count]
|
|
|
|
grid = [[""] * board_size for _ in range(board_size)]
|
|
placed_words = []
|
|
|
|
for cw in selected:
|
|
result = self._place_word(grid, cw.word, board_size)
|
|
if result:
|
|
row, col, direction = result
|
|
placed_words.append(
|
|
Word(
|
|
text=cw.word,
|
|
start_row=row,
|
|
start_col=col,
|
|
direction=direction,
|
|
)
|
|
)
|
|
|
|
# Fill empty cells with random letters
|
|
for r in range(board_size):
|
|
for c in range(board_size):
|
|
if grid[r][c] == "":
|
|
grid[r][c] = random.choice(string.ascii_uppercase)
|
|
|
|
game = Game(
|
|
category_id=category_id,
|
|
board_size=board_size,
|
|
grid=grid,
|
|
)
|
|
|
|
for w in placed_words:
|
|
w.game = game
|
|
|
|
return game, placed_words
|
|
|
|
def _place_word(self, grid, word, board_size):
|
|
directions = list(DIRECTIONS.keys())
|
|
for _ in range(MAX_PLACEMENT_ATTEMPTS):
|
|
direction = random.choice(directions)
|
|
dr, dc = DIRECTIONS[direction]
|
|
word_len = len(word)
|
|
|
|
# Calculate valid start ranges
|
|
if dr > 0:
|
|
row_range = range(0, board_size - word_len + 1)
|
|
elif dr < 0:
|
|
row_range = range(word_len - 1, board_size)
|
|
else:
|
|
row_range = range(0, board_size)
|
|
|
|
if dc > 0:
|
|
col_range = range(0, board_size - word_len + 1)
|
|
elif dc < 0:
|
|
col_range = range(word_len - 1, board_size)
|
|
else:
|
|
col_range = range(0, board_size)
|
|
|
|
if not row_range or not col_range:
|
|
continue
|
|
|
|
row = random.choice(list(row_range))
|
|
col = random.choice(list(col_range))
|
|
|
|
if self._can_place(grid, word, row, col, dr, dc):
|
|
self._do_place(grid, word, row, col, dr, dc)
|
|
return row, col, direction
|
|
|
|
return None
|
|
|
|
def _can_place(self, grid, word, row, col, dr, dc):
|
|
for i, letter in enumerate(word):
|
|
r = row + i * dr
|
|
c = col + i * dc
|
|
if grid[r][c] != "" and grid[r][c] != letter:
|
|
return False
|
|
return True
|
|
|
|
def _do_place(self, grid, word, row, col, dr, dc):
|
|
for i, letter in enumerate(word):
|
|
r = row + i * dr
|
|
c = col + i * dc
|
|
grid[r][c] = letter
|