Files
wordsearch/game/factory.py
T
Mathew Sir Guest the bestandClaude Opus 4.6 4d681a92ca docs: add Google-style docstrings and Sphinx autodoc setup
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>
2026-05-06 23:06:37 -06:00

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