Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update README example #38

Merged
merged 1 commit into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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