class CrosswordPuzzle(object): """A crossword puzzle grid that automatically generates puzzles. A `m` by `n` grid is populated with words from Wordnik's corpus. Currently the resulting crossword puzzle cannot have parallel, adjacent words like you would find in the NY TImes crossword. The grid itself is accessible through the __getitem__ method of the puzzle, which provides access to the individual Squares. Clues are taken from Wordnik (definitions, example usages, synonyms, etc.) and are stored in a dictionary from clue positions to words and clues: self.clues[1, 'DOWN'] => ('cat', 'A small domestic animal') In order to create the puzzle you can use the populate_puzzle method, which uses Wordnik's Word of the Day as the first word and then adds the specified number of words to the puzzle. """ def __init__(self, rows=15, columns=15, api_key=None): """Create a `rows` X `columns` grid and initialize the clues dict. If `api_key` is not set then the key in config.py is tried. """ self.grid = Grid(rows, columns) self.clues = {} api_key = api_key or config.WORDNIK_API_KEY if not api_key: raise WordnikAPIKeyError('Enter your Wordnik API key in config.py') self.wordnik = Wordnik(api_key) self._current_sq_id = 1 # To keep track of Square IDs def __str__(self): """Return the grid as a string.""" return str(self.grid) def populate_puzzle(self, word_count): """Try to `word_count` words/clues. Return the number of words added.""" words_added = 0 if not self.clues: self.place_first_word() word_count -= 1 words_added += 1 for i in range(word_count): result = self.find_and_add_a_word() if result is None: s = 'Grid filled up after adding %d words.' % len(self.clues) print >> sys.stderr, s break else: words_added += 1 self.finalize() return words_added def place_first_word(self, word=None): """Add the Wordnik Word of the Day as the first word in the puzzle. If no word is passed in, the Wordnik Word of the Day is used. """ if word is None: word = self.wordnik.word_of_the_day()['wordstring'] #TODO: handle the WOTD being too long assert len(word) <= self.grid.num_columns, 'First word is too long.' span = [(0, n) for n in range(len(word))] self.add_word(word, span) def find_and_add_a_word(self): """Find a word in the Wordnik corpus that fits the puzzle and add it. If the search and addition are successful, return the wordstring. If not, return None. """ open_spans = sorted(self.grid.open_spans(), key=len, reverse=True) for span in open_spans: query = ''.join([str(self.grid[m, n]) for (m, n) in span]) query = query.replace(' ', '?') length = len(query) words = self.wordnik.word_search(query, max_length=length, min_dictionary_count=1) if words: word = max(words, key=lambda w: w['count']) self.add_word(word['wordstring'], span) return word['wordstring'] return None def store_clue(self, word, id_, direction, clue): """Store a word in self.clues. Call after putting word on the grid.""" self.clues[id_, direction] = (word, clue) def add_word(self, word, span): """Place the word on the grid then add it and its clue to self.clues.""" print >> sys.stderr, 'Placing word "%s".' % word self.put_word_on_grid(word, span) m, n = span[0][0], span[0][1] first_square = self.grid[m, n] if first_square.id_ is None: id_ = self._current_sq_id self._current_sq_id += 1 first_square.id_ = id_ else: id_ = first_square.id_ definitions = self.wordnik.definitions(word) definition = random.choice(definitions)['text'] direction = self.grid.get_span_direction(span) self.store_clue(word, id_, direction, definition) def put_word_on_grid(self, word, span): """Add the nth letter in `word` to the nth position in `span`. """ assert len(word) == len(span) assert len(span) > 1, "Can't insert word shorter than two letters." for i, char in enumerate(word): (m, n) = span[i] if self.grid[m, n].letter is None: self.grid[m, n].letter = char else: assert self.grid[m, n].letter == char # Black out open squares on either end of the word if they exist. direction = self.grid.get_span_direction(span) if direction == 'ACROSS': for (m, n) in ((m, n - 1), (m, n + 1)): if self.grid.are_valid_coordinates(m, n): self.grid.blackout_square(m, n) elif direction == 'DOWN': for (m, n) in ((m - 1, n), (m + 1, n)): if self.grid.are_valid_coordinates(m, n): self.grid.blackout_square(m, n) else: assert False, "Sanity check" def finalize(self): """Perform cleanup after all the words have been placed.""" self.grid.blackout_all_open_squares() # # Gameplay related methods # @property def is_completed(self): """Return True if the user's entries match the correct letters.""" for sq in self.grid: if sq.letter != sq.user_entry: return False return True def enter_from_user(self, m, n, letter): """Set the value of the square at (`m`, `n`) to `letter`.""" sq = self.grid[m, n] assert not sq.is_blacked_out sq.letter = letter