There's probably a better title for this proposal, as it's broader than that. I'd also be happy to work on coding this, but I want to discuss it first—especially considering the big refactor since the last time I poked at this project.
The overall goal is to increase the Game object's flexibility by decoupling some word placement assumptions. Ideally, the WordSearch API will remain unchanged by these under-the-hood changes.
New attrs for the Word object
priority: int = 3
Instead of baking the algorithm of first placing the regular words before secret words into the Game object, assign each word a priority.
#pseudocode for Game object methods
def words_by_priority_dict(self) -> dict[int, list[Word]]:
# there's almost certainly an easier way to do this with itertools
# or at least a clever one-liner
d = defaultdict(list)
for w in self.words:
d[w.priority].append(w)
return d
def list_words_by_priority(self) -> list[Word]:
# see above comment, probably doesn't need 2 functions
o = []
for pri in self.words_by_priority_dict().values():
o += sorted(pri, key=lambda w: len(w), reverse=True)
return o
Priority 1 words go first, then priority 2, etc…
Within each priority, the default sort is longest to shortest.
For the existing behavior of the WordSearch object, regular words should receive priority=2 and secret words priority=4.
valid_dirs: DirectionSet | None = None
(If set to None, should the word be skipped during placement or treated as a wildcard? Personally, I'd lean toward skipped.)
An alternate implementation would be to pass a dict[int, DirectionSet]
to the Game object where the valid directions for each priority are shared. However, I feel that it's best for subclasses of Game to assign priorities to the Words during ingestion rather than forcing all words of the same priority to share the same valid directions in the Game class.
Moving the valid_dirs attr to the Word also frees up the Generator class to drop the directions & secret_directions params from its generate function, as that data is now contained within each Word.
For the WordSearch, assign either directions or secret_directions during Word creation as appropriate.
Conflict with existing design in the above
The Game class takes its words and secret_words as strings instead of iterables of Word objects. Perhaps I'll think of a better location for the ingestion & creation of Word objects after I've gotten some sleep and thought more about Python's init order. The init of a Game updated to reflect this proposal would look something like __init__(words, size, *, ...)
. However, the init for its subclasses should remain largely as they currently are.
Duplicate word policy
Perhaps this is addressed in existing code, but Game could accept an optional parameter of a Callable for duplicate_policy. Perhaps it could be a Validator… would need to look at it with freshly-caffeinated neurons. These would be made on Word objects after they have been assigned valid directions and priorities.
Three policies spring to mind:
- Error. Raises an error when it encounters a duplicate word.
- Merge: (see below pseudocode)
- drop(first=True) keeps either the oldest or most recent duplicate.
# this modifies w1 so the init doesn't need more fields
def merge(w1: Word, w2: Word) -> None:
w1.secret = w1.secret and w2.secret
w1.valid_directions |= w2.valid_directions
w1.priority = min(w1.priority, w2.priority)
Word len
This doesn't require any of the above to implement
def __len__(self) -> int:
return len(self.text)