Skip to content

Commit

Permalink
update readme example (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
pmaher86 authored Oct 12, 2024
1 parent 769882c commit 861ae5c
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 29 deletions.
80 changes: 52 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,21 +110,38 @@ ScoredWord(word='MACDUFF', score=25.387070408819042)
Custom word lists are supported and can be passed into the `Crossword` constructor or any of the solving methods. The default word list used is the [Crossword Nexus Collaborative Word List](https://github.com/Crossword-Nexus/collaborative-word-list).

## Example: full symmetry puzzles
As an example of how blacksquare's abstractions allow for non-trivial crossword construction, consider the [June 6 2023 NYT puzzle](https://www.xwordinfo.com/Crossword?date=6/6/2023), which displays not only a rotationaly symmetric grid but a rotationally symmetric *fill*. While this might seem daunting to build, all we have to do is override the `set_word` method of `Crossword` to fill two words at once, and then restrict our wordlist to emordnilaps (words that are also a word when reversed).
As an example of how blacksquare's abstractions allow for non-trivial crossword construction, consider the [June 6 2023 NYT puzzle](https://www.xwordinfo.com/Crossword?date=6/6/2023), which displays not only a rotationaly symmetric grid but a rotationally symmetric *fill*. While this might seem daunting to build, all we have to do is override a couple methods of the base Crossword class, and use some modified wordlists.
```python
class SymmetricCrossword(Crossword):
def set_word(self, word_index: WordIndex, value: str) -> None:
class SymmetricCrossword(bs.Crossword):
# This sets symmetric words to be mirror images
def set_word(self, word_index, value):
super().set_word(word_index, value)
super().set_word(self.get_symmetric_word_index(word_index), value[::-1])

emordilaps = {}
for word, score in tqdm(bs.DEFAULT_WORDLIST):
reverse_score = bs.DEFAULT_WORDLIST.get_score(word[::-1])
if reverse_score:
emordilaps[word] = min(score, reverse_score)
emordilaps_wordlist = bs.WordList(emordilaps)

# Now just construct the puzzle and fill!
# This makes it so that we only track a unique half of the puzzle in the dependency
# graph (needed for the fill algorithm).
def get_disconnected_open_subgrids(self):
subgrids = super().get_disconnected_open_subgrids()
new_subgrids = []
for sg in subgrids:
new_sg = sorted([min(i, self.get_symmetric_word_index(i)) for i in sg])
if new_sg not in new_subgrids:
new_subgrids.append(new_sg)
return new_subgrids

palindromes = {}
emordnilaps = {}
for word, score in bs.DEFAULT_WORDLIST:
if word == word[::-1]:
palindromes[word] = score
else:
reverse_score = bs.DEFAULT_WORDLIST.get_score(word[::-1])
if reverse_score:
emordnilaps[word] = min(score, reverse_score)
palindrome_wordlist = bs.WordList(palindromes)
emordnilap_wordlist = bs.WordList(emordnilaps)

# Now construct the grid
xw = SymmetricCrossword(15)
filled = [
(0, 3), (0, 4), (0, 5), (0, 11), (1, 4), (1, 5), (1, 11),
Expand All @@ -134,41 +151,48 @@ filled = [
]
for i in filled:
xw[i] = bs.BLACK
xw.fill(emordnilap_wordlist, temperature=0.3)

# Now fill the central words with palindromes
central_words = [w for w in xw.iterwords() if w.symmetric_image.index == w.index]
for cw in central_words:
cw.value = palindrome_wordlist.find_matches(cw)[0].word

# And the rest!
xw.fill(emordnilap_wordlist, score_filter=0.3)

┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
^F^E^N │███│███│███│^S^N^I^P^S │███│^E^D^A
^R^O^M │███│███│███│^A^G^A^R^D │███│^M^E^D
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
^LI │ A │^R │███│███│^POS │ E │ A │███│^VER
^ED │ A │^M │███│███│^RAT │ E │ R │███│^ADU
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
^OKIE │███│^REWARD │███│^ALB
^NANU │███│^LAMINA │███│^OIC
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
^GORT │███│^ATI │ N │███│^D^ELIA
^EYER │███│^OME │ N │███│^W^ARTS
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
│███│███│███│^R^A │ PS │███│███│^R │ E │ ES │ A │███│
│███│███│███│^D^A │ TA │███│███│^S │ E │ RI │ A │███│
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
^S │^T^ROP │ S │███│^S │^P │ A │ NK │███│███│███│
^S │^L^EET │ S │███│^S │^M │ A │ RT │███│███│███│
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
^R │ E │ ES │ A │███│^S │ T │ OM │███│^S^E^P^S
^T │ E │ RR │ A │███│^S │ T │ AB │███│^A^S^O^P
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
^ARM │███│^R │^OTATO^R │███│^MRA
^EBO │███│^R │^ACECA^R │███│^OBE
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
^SPE^S │███│^MO │ T │ S │███│^A │^SE │ E │ R
^POS^A │███│^BA │ T │ S │███│^A │^RR │ E │ T
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
│███│███│███│^K^N │ A │ P │ S │███│^S │ PORT │ S │
│███│███│███│^T^R │ A │ M │ S │███│^S │ TEEL │ S │
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
│███│^A │^SE │ E │ R │███│███│^SP │ A │ R │███│███│███│
│███│^A │^IR │ E │ S │███│███│^AT │ A │ D │███│███│███│
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
^AILED │███│^N │^ITA │███│^T^R^O^G
^STRAW │███│^N │^EMO │███│^R^E^Y^E
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
^BLA │███│^D^RAWER │███│^EIKO
^CIO │███│^A^NIMAL │███│^UNAN
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
^REV │███│^A │ E │ SOP │███│███│^R │ A │ IL
^UDA │███│^R │ E │ TAR │███│███│^M │ A │ DE
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
^ADE │███│^SPINS │███│███│███│^NEF
^DEM │███│^DRAGA │███│███│███│^MOR
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
```
There's clearly some extra curation that could be done to improve the word list, and we'd need a little more logic to avoid repeat fills and using true palindromes outside of the center. But not bad for a few lines of code!
There's clearly some extra curation that could be done to improve the word list, but not bad for a couple dozen lines of code!

## Installation
`pip install blacksquare`
Expand Down
4 changes: 3 additions & 1 deletion src/blacksquare/crossword.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ def copy(self) -> Crossword:
"""
return copy.deepcopy(self)

def get_disconnected_open_subgrids(self) -> List[List[Word]]:
def get_disconnected_open_subgrids(self) -> List[List[WordIndex]]:
"""Returns a list of open subgrids, as represented by a list of words. An open
subgrid is a set of words whose fill can in principle depend on each other. For
instance, if the only the northwest and southeast corners are a puzzle are open,
Expand Down Expand Up @@ -677,6 +677,7 @@ def recurse_subgraph_fill(
old_value = word_to_match.value
# temp fill for subgraph calculation
xw[word_to_match.index] = noisy_matches.words[0]
display_context.update(xw._text_grid())
new_subgraphs = [
s
for s in xw.get_disconnected_open_subgrids()
Expand All @@ -695,6 +696,7 @@ def recurse_subgraph_fill(
else:
return True
xw[word_to_match.index] = old_value
dead_end_states.add(xw.hashable_state(active_subgraph))
return False

with Live(self._text_grid(), refresh_per_second=4, transient=True) as live:
Expand Down
2 changes: 2 additions & 0 deletions src/blacksquare/word_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ def _repr_html_(self):
return self.frame._repr_html_()

def __getitem__(self, key) -> ScoredWord:
if not isinstance(key, int):
raise IndexError
return ScoredWord(self._words[key], self._scores[key])

def __iter__(self):
Expand Down

0 comments on commit 861ae5c

Please sign in to comment.