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