feat: add BoardFactory for word search grid generation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1c4154b84b
commit
e483d1127a
+115
@@ -0,0 +1,115 @@
|
||||
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:
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
|
||||
def create(self, category_id, board_size=10, word_count=8):
|
||||
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
|
||||
@@ -0,0 +1,89 @@
|
||||
import random
|
||||
|
||||
from models.category import Category, CategoryWord
|
||||
from game.factory import BoardFactory
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
|
||||
def _seed_category(db_session, name, words):
|
||||
cat = Category(name=name)
|
||||
db_session.add(cat)
|
||||
db_session.flush()
|
||||
for w in words:
|
||||
db_session.add(CategoryWord(category_id=cat.id, word=w))
|
||||
db_session.commit()
|
||||
return cat
|
||||
|
||||
|
||||
def test_factory_creates_game_and_words(db_session):
|
||||
cat = _seed_category(
|
||||
db_session,
|
||||
"animals",
|
||||
["TIGER", "LION", "BEAR", "WOLF", "EAGLE", "HAWK", "DEER", "FROG"],
|
||||
)
|
||||
factory = BoardFactory(db_session)
|
||||
game, words = factory.create(category_id=cat.id, board_size=10, word_count=5)
|
||||
assert game.board_size == 10
|
||||
assert game.category_id == cat.id
|
||||
assert len(game.grid) == 10
|
||||
assert all(len(row) == 10 for row in game.grid)
|
||||
assert len(words) <= 5
|
||||
assert len(words) >= 1
|
||||
|
||||
|
||||
def test_factory_words_are_on_grid(db_session):
|
||||
cat = _seed_category(
|
||||
db_session,
|
||||
"test",
|
||||
["CAT", "DOG", "BAT", "RAT", "OWL", "COW", "PIG", "HEN"],
|
||||
)
|
||||
random.seed(42)
|
||||
factory = BoardFactory(db_session)
|
||||
game, words = factory.create(category_id=cat.id, board_size=10, word_count=5)
|
||||
for word in words:
|
||||
dr, dc = DIRECTIONS[word.direction]
|
||||
extracted = ""
|
||||
r, c = word.start_row, word.start_col
|
||||
for _ in range(len(word.text)):
|
||||
extracted += game.grid[r][c]
|
||||
r += dr
|
||||
c += dc
|
||||
assert extracted == word.text
|
||||
|
||||
|
||||
def test_factory_fills_all_cells(db_session):
|
||||
cat = _seed_category(db_session, "small", ["AB", "CD"])
|
||||
factory = BoardFactory(db_session)
|
||||
game, _ = factory.create(category_id=cat.id, board_size=8, word_count=2)
|
||||
for row in game.grid:
|
||||
for cell in row:
|
||||
assert cell.isalpha() and cell.isupper() and len(cell) == 1
|
||||
|
||||
|
||||
def test_factory_filters_words_too_long(db_session):
|
||||
cat = _seed_category(
|
||||
db_session,
|
||||
"mixed",
|
||||
["HIPPOPOTAMUS", "CAT", "DOG"], # HIPPOPOTAMUS won't fit in 8x8
|
||||
)
|
||||
factory = BoardFactory(db_session)
|
||||
game, words = factory.create(category_id=cat.id, board_size=8, word_count=3)
|
||||
placed_texts = [w.text for w in words]
|
||||
assert "HIPPOPOTAMUS" not in placed_texts
|
||||
|
||||
|
||||
def test_factory_clamps_word_count(db_session):
|
||||
cat = _seed_category(db_session, "tiny", ["CAT", "DOG"])
|
||||
factory = BoardFactory(db_session)
|
||||
game, words = factory.create(category_id=cat.id, board_size=10, word_count=10)
|
||||
assert len(words) <= 2
|
||||
Reference in New Issue
Block a user